diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts index 6eec19ca100..30ef86e8269 100644 --- a/integration/single-fetch-test.ts +++ b/integration/single-fetch-test.ts @@ -90,7 +90,6 @@ test.describe("single-fetch", () => { test.beforeEach(() => { oldConsoleError = console.error; - console.error = () => {}; }); test.afterEach(() => { @@ -138,6 +137,8 @@ test.describe("single-fetch", () => { }); test("loads proper errors on single fetch loader requests", async () => { + console.error = () => {}; + let fixture = await createFixture( { config: { @@ -584,6 +585,251 @@ test.describe("single-fetch", () => { expect(res.headers.get("x-c-headers")).toEqual("true"); }); + test("processes loader redirects", async ({ page }) => { + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/data.tsx": js` + import { redirect } from '@remix-run/node'; + export function loader() { + return redirect('/target'); + } + export default function Component() { + return null + } + `, + "app/routes/target.tsx": js` + export default function Component() { + return

Target

+ } + `, + }, + }); + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink("/data"); + await page.waitForSelector("#target"); + expect(await app.getHtml("#target")).toContain("Target"); + }); + + test("processes action redirects", async ({ page }) => { + let fixture = await createFixture( + { + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/routes/data.tsx": js` + import { redirect } from '@remix-run/node'; + export function action() { + return redirect('/target'); + } + export default function Component() { + return null + } + `, + "app/routes/target.tsx": js` + export default function Component() { + return

Target

+ } + `, + }, + }, + ServerMode.Development + ); + let appFixture = await createAppFixture(fixture, ServerMode.Development); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickSubmitButton("/data"); + await page.waitForSelector("#target"); + expect(await app.getHtml("#target")).toContain("Target"); + }); + + test("processes redirects from handleDataRequest (after loaders)", async ({ + page, + }) => { + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/entry.server.tsx": js` + import { PassThrough } from "node:stream"; + + import type { EntryContext } from "@remix-run/node"; + import { createReadableStreamFromReadable } from "@remix-run/node"; + import { RemixServer } from "@remix-run/react"; + import { renderToPipeableStream } from "react-dom/server"; + + export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext + ) { + return new Promise((resolve, reject) => { + const { pipe } = renderToPipeableStream( + , + { + onShellReady() { + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + responseHeaders.set("Content-Type", "text/html"); + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + }, + } + ); + }); + } + + export function handleDataRequest(response, { request }) { + if (request.url.endsWith("/data.data")) { + return new Response(null, { + status: 302, + headers: { + Location: "/target", + }, + }); + } + return response; + } + `, + "app/routes/data.tsx": js` + import { redirect } from '@remix-run/node'; + export function loader() { + return redirect('/target'); + } + export default function Component() { + return null + } + `, + "app/routes/target.tsx": js` + export default function Component() { + return

Target

+ } + `, + }, + }); + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink("/data"); + await page.waitForSelector("#target"); + expect(await app.getHtml("#target")).toContain("Target"); + }); + + test("processes redirects from handleDataRequest (after actions)", async ({ + page, + }) => { + let fixture = await createFixture({ + config: { + future: { + unstable_singleFetch: true, + }, + }, + files: { + ...files, + "app/entry.server.tsx": js` + import { PassThrough } from "node:stream"; + + import type { EntryContext } from "@remix-run/node"; + import { createReadableStreamFromReadable } from "@remix-run/node"; + import { RemixServer } from "@remix-run/react"; + import { renderToPipeableStream } from "react-dom/server"; + + export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext + ) { + return new Promise((resolve, reject) => { + const { pipe } = renderToPipeableStream( + , + { + onShellReady() { + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + responseHeaders.set("Content-Type", "text/html"); + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + }, + } + ); + }); + } + + export function handleDataRequest(response, { request }) { + if (request.url.endsWith("/data.data")) { + return new Response(null, { + status: 302, + headers: { + Location: "/target", + }, + }); + } + return response; + } + `, + "app/routes/data.tsx": js` + import { redirect } from '@remix-run/node'; + export function action() { + return redirect('/target'); + } + export default function Component() { + return null + } + `, + "app/routes/target.tsx": js` + export default function Component() { + return

Target

+ } + `, + }, + }); + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickSubmitButton("/data"); + await page.waitForSelector("#target"); + expect(await app.getHtml("#target")).toContain("Target"); + }); + test.describe("client loaders", () => { test("when no routes have client loaders", async ({ page }) => { let fixture = await createFixture( diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index ccf4c3e6ce4..65da52b4e39 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -80,6 +80,8 @@ function derive(build: ServerBuild, mode?: string) { }; } +export const SingleFetchRedirectSymbol = Symbol("SingleFetchRedirect"); + export const createRequestHandler: CreateRequestHandlerFunction = ( build, mode @@ -190,7 +192,29 @@ export const createRequestHandler: CreateRequestHandlerFunction = ( }); if (isRedirectResponse(response)) { - response = createRemixRedirectResponse(response, _build.basename); + let result: SingleFetchResult | SingleFetchResults = + getSingleFetchRedirect(response); + + if (request.method === "GET") { + result = { + [SingleFetchRedirectSymbol]: result, + }; + } + let headers = new Headers(response.headers); + headers.set("Content-Type", "text/x-turbo"); + + return new Response( + encodeViaTurboStream( + result, + request.signal, + _build.entry.module.streamTimeout, + serverMode + ), + { + status: 200, + headers, + } + ); } } } else if ( @@ -300,8 +324,6 @@ async function handleDataRequest( } } -export const SingleFetchRedirectSymbol = Symbol("SingleFetchRedirect"); - type SingleFetchRedirectResult = { redirect: string; status: number;