diff --git a/integration/redirects-test.ts b/integration/redirects-test.ts index cc60961874..9c71b4b2c5 100644 --- a/integration/redirects-test.ts +++ b/integration/redirects-test.ts @@ -150,6 +150,42 @@ test.describe("redirects", () => { return
+ Search Params: {location.search} +
+ > + ); + } + `, }, }); @@ -224,6 +260,49 @@ test.describe("redirects", () => { await page.goBack(); await page.waitForSelector("#a"); // [/a] }); + + test("maintains custom headers on redirects", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + + let hasGetHeader = false; + let hasPostHeader = false; + page.on("request", async (request) => { + let extension = /^rsc/.test(templateName) ? "rsc" : "data"; + if ( + request.method() === "GET" && + request.url().endsWith(`headers.${extension}?redirect=`) + ) { + const headers = (await request.response())?.headers() || {}; + hasGetHeader = headers["x-test"] === "Foo"; + } + if ( + request.method() === "POST" && + request.url().endsWith(`headers.${extension}`) + ) { + const headers = (await request.response())?.headers() || {}; + hasPostHeader = headers["x-test"] === "Foo"; + } + }); + + await app.goto("/headers", true); + await app.clickElement("#loader-redirect"); + await expect(page.locator("#search-params")).toHaveText( + "Search Params: ?loader-redirect" + ); + expect(hasGetHeader).toBe(true); + expect(hasPostHeader).toBe(false); + + hasGetHeader = false; + hasPostHeader = false; + + await app.goto("/headers", true); + await app.clickElement("#action-redirect"); + await expect(page.locator("#search-params")).toHaveText( + "Search Params: ?action-redirect" + ); + expect(hasGetHeader).toBe(false); + expect(hasPostHeader).toBe(true); + }); }); } }); diff --git a/packages/react-router/lib/rsc/server.rsc.ts b/packages/react-router/lib/rsc/server.rsc.ts index 21362593f5..b06267b228 100644 --- a/packages/react-router/lib/rsc/server.rsc.ts +++ b/packages/react-router/lib/rsc/server.rsc.ts @@ -649,13 +649,24 @@ function generateRedirectResponse( status: response.status, actionResult, }; + + // Preserve non-internal headers on the user-created redirect + let headers = new Headers(response.headers); + headers.delete("Location"); + headers.delete("X-Remix-Reload-Document"); + headers.delete("X-Remix-Replace"); + // Remove Content-Length because node:http will truncate the response body + // to match the Content-Length header, which can result in incomplete data + // if the actual encoded body is longer. + // https://nodejs.org/api/http.html#class-httpclientrequest + headers.delete("Content-Length"); + headers.set("Content-Type", "text/x-component"); + headers.set("Vary", "Content-Type"); + return generateResponse( { statusCode: SINGLE_FETCH_REDIRECT_STATUS, - headers: new Headers({ - "Content-Type": "text/x-component", - Vary: "Content-Type", - }), + headers, payload, }, { temporaryReferences } @@ -708,6 +719,12 @@ async function generateStaticContextResponse( (match) => (match as RouteMatch