Skip to content

Commit d47e692

Browse files
authored
Preserve headers on action redirects (#13920)
* Preserve headers on action redirects * Add test
1 parent 4d458eb commit d47e692

File tree

2 files changed

+100
-4
lines changed

2 files changed

+100
-4
lines changed

integration/redirects-test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,42 @@ test.describe("redirects", () => {
150150
return <h1 id="d">D</h1>
151151
}
152152
`,
153+
154+
"app/routes/headers.tsx": js`
155+
import * as React from 'react';
156+
import { Link, Form, redirect, useLocation } from 'react-router';
157+
158+
export function action() {
159+
return redirect('/headers?action-redirect', {
160+
headers: { 'X-Test': 'Foo' }
161+
})
162+
}
163+
164+
export function loader({ request }) {
165+
let url = new URL(request.url);
166+
if (url.searchParams.has('redirect')) {
167+
return redirect('/headers?loader-redirect', {
168+
headers: { 'X-Test': 'Foo' }
169+
})
170+
}
171+
return null
172+
}
173+
174+
export default function Component() {
175+
let location = useLocation()
176+
return (
177+
<>
178+
<Link id="loader-redirect" to="/headers?redirect">Redirect</Link>
179+
<Form method="post">
180+
<button id="action-redirect" type="submit">Action Redirect</button>
181+
</Form>
182+
<p id="search-params">
183+
Search Params: {location.search}
184+
</p>
185+
</>
186+
);
187+
}
188+
`,
153189
},
154190
});
155191

@@ -224,6 +260,49 @@ test.describe("redirects", () => {
224260
await page.goBack();
225261
await page.waitForSelector("#a"); // [/a]
226262
});
263+
264+
test("maintains custom headers on redirects", async ({ page }) => {
265+
let app = new PlaywrightFixture(appFixture, page);
266+
267+
let hasGetHeader = false;
268+
let hasPostHeader = false;
269+
page.on("request", async (request) => {
270+
let extension = /^rsc/.test(templateName) ? "rsc" : "data";
271+
if (
272+
request.method() === "GET" &&
273+
request.url().endsWith(`headers.${extension}?redirect=`)
274+
) {
275+
const headers = (await request.response())?.headers() || {};
276+
hasGetHeader = headers["x-test"] === "Foo";
277+
}
278+
if (
279+
request.method() === "POST" &&
280+
request.url().endsWith(`headers.${extension}`)
281+
) {
282+
const headers = (await request.response())?.headers() || {};
283+
hasPostHeader = headers["x-test"] === "Foo";
284+
}
285+
});
286+
287+
await app.goto("/headers", true);
288+
await app.clickElement("#loader-redirect");
289+
await expect(page.locator("#search-params")).toHaveText(
290+
"Search Params: ?loader-redirect"
291+
);
292+
expect(hasGetHeader).toBe(true);
293+
expect(hasPostHeader).toBe(false);
294+
295+
hasGetHeader = false;
296+
hasPostHeader = false;
297+
298+
await app.goto("/headers", true);
299+
await app.clickElement("#action-redirect");
300+
await expect(page.locator("#search-params")).toHaveText(
301+
"Search Params: ?action-redirect"
302+
);
303+
expect(hasGetHeader).toBe(false);
304+
expect(hasPostHeader).toBe(true);
305+
});
227306
});
228307
}
229308
});

packages/react-router/lib/rsc/server.rsc.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -717,13 +717,24 @@ function generateRedirectResponse(
717717
status: response.status,
718718
actionResult,
719719
};
720+
721+
// Preserve non-internal headers on the user-created redirect
722+
let headers = new Headers(response.headers);
723+
headers.delete("Location");
724+
headers.delete("X-Remix-Reload-Document");
725+
headers.delete("X-Remix-Replace");
726+
// Remove Content-Length because node:http will truncate the response body
727+
// to match the Content-Length header, which can result in incomplete data
728+
// if the actual encoded body is longer.
729+
// https://nodejs.org/api/http.html#class-httpclientrequest
730+
headers.delete("Content-Length");
731+
headers.set("Content-Type", "text/x-component");
732+
headers.set("Vary", "Content-Type");
733+
720734
return generateResponse(
721735
{
722736
statusCode: SINGLE_FETCH_REDIRECT_STATUS,
723-
headers: new Headers({
724-
"Content-Type": "text/x-component",
725-
Vary: "Content-Type",
726-
}),
737+
headers,
727738
payload,
728739
},
729740
{ temporaryReferences }
@@ -777,6 +788,12 @@ async function generateStaticContextResponse(
777788
(match) => (match as RouteMatch<string, RSCRouteConfigEntry>).route.headers
778789
);
779790

791+
// Remove Content-Length because node:http will truncate the response body
792+
// to match the Content-Length header, which can result in incomplete data
793+
// if the actual encoded body is longer.
794+
// https://nodejs.org/api/http.html#class-httpclientrequest
795+
headers.delete("Content-Length");
796+
780797
const baseRenderPayload: Omit<RSCRenderPayload, "matches" | "patches"> = {
781798
type: "render",
782799
basename,

0 commit comments

Comments
 (0)