Skip to content

Commit 23db292

Browse files
brophdawg11machour
andauthored
fix: preserve method on 307/308 redirects (#9597)
* fix: preserve method on 307/308 redirects * Add changeset * Fix changeset Co-authored-by: Mehdi Achour <[email protected]> * Strengthen HTTP method validation * Move redirect status codes to a constant * PR feedback * bundle bump Co-authored-by: Mehdi Achour <[email protected]>
1 parent d6a6825 commit 23db292

File tree

5 files changed

+360
-70
lines changed

5 files changed

+360
-70
lines changed

.changeset/stale-coats-smoke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@remix-run/router": patch
3+
---
4+
5+
Preserve the HTTP method on 307/308 redirects

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
"none": "10.5 kB"
120120
},
121121
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
122-
"none": "16 kB"
122+
"none": "16.5 kB"
123123
}
124124
}
125125
}

packages/router/__tests__/router-test.ts

Lines changed: 253 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,11 @@ function setup({
365365
let redirectNavigationId = ++guid;
366366
activeLoaderType = "navigation";
367367
activeLoaderNavigationId = redirectNavigationId;
368+
if (status === 307 || status === 308) {
369+
activeActionType = "navigation";
370+
activeActionNavigationId = redirectNavigationId;
371+
}
372+
368373
let helpers = getNavigationHelpers(href, redirectNavigationId);
369374

370375
// Since a redirect kicks off and awaits a new navigation we can't shim
@@ -3966,6 +3971,86 @@ describe("a router", () => {
39663971
});
39673972
});
39683973

3974+
it("returns a 405 error if attempting to submit with method=HEAD", async () => {
3975+
let t = setup({
3976+
routes: TASK_ROUTES,
3977+
initialEntries: ["/"],
3978+
hydrationData: {
3979+
loaderData: {
3980+
root: "ROOT DATA",
3981+
index: "INDEX DATA",
3982+
},
3983+
},
3984+
});
3985+
3986+
let formData = new FormData();
3987+
formData.append(
3988+
"blob",
3989+
new Blob(["<h1>Some html file contents</h1>"], {
3990+
type: "text/html",
3991+
})
3992+
);
3993+
3994+
await t.navigate("/tasks", {
3995+
// @ts-expect-error
3996+
formMethod: "head",
3997+
formData: formData,
3998+
});
3999+
expect(t.router.state.navigation.state).toBe("idle");
4000+
expect(t.router.state.location).toMatchObject({
4001+
pathname: "/tasks",
4002+
search: "",
4003+
});
4004+
expect(t.router.state.errors).toEqual({
4005+
tasks: new ErrorResponse(
4006+
405,
4007+
"Method Not Allowed",
4008+
new Error('Invalid request method "HEAD"'),
4009+
true
4010+
),
4011+
});
4012+
});
4013+
4014+
it("returns a 405 error if attempting to submit with method=OPTIONS", async () => {
4015+
let t = setup({
4016+
routes: TASK_ROUTES,
4017+
initialEntries: ["/"],
4018+
hydrationData: {
4019+
loaderData: {
4020+
root: "ROOT DATA",
4021+
index: "INDEX DATA",
4022+
},
4023+
},
4024+
});
4025+
4026+
let formData = new FormData();
4027+
formData.append(
4028+
"blob",
4029+
new Blob(["<h1>Some html file contents</h1>"], {
4030+
type: "text/html",
4031+
})
4032+
);
4033+
4034+
await t.navigate("/tasks", {
4035+
// @ts-expect-error
4036+
formMethod: "options",
4037+
formData: formData,
4038+
});
4039+
expect(t.router.state.navigation.state).toBe("idle");
4040+
expect(t.router.state.location).toMatchObject({
4041+
pathname: "/tasks",
4042+
search: "",
4043+
});
4044+
expect(t.router.state.errors).toEqual({
4045+
tasks: new ErrorResponse(
4046+
405,
4047+
"Method Not Allowed",
4048+
new Error('Invalid request method "OPTIONS"'),
4049+
true
4050+
),
4051+
});
4052+
});
4053+
39694054
it("runs loaders above the boundary for 400 errors if binary data is attempted to be submitted using formMethod=GET", async () => {
39704055
let t = setup({
39714056
routes: [
@@ -5515,6 +5600,173 @@ describe("a router", () => {
55155600
window.location = oldLocation;
55165601
}
55175602
});
5603+
5604+
describe("redirect status code handling", () => {
5605+
it("should not treat 300 as a redirect", async () => {
5606+
let t = setup({ routes: REDIRECT_ROUTES });
5607+
5608+
let A = await t.navigate("/parent");
5609+
await A.loaders.parent.redirectReturn("/idk", 300);
5610+
expect(t.router.state).toMatchObject({
5611+
loaderData: {},
5612+
location: {
5613+
pathname: "/parent",
5614+
},
5615+
navigation: {
5616+
state: "idle",
5617+
},
5618+
});
5619+
});
5620+
5621+
it("should not preserve the method on 301 redirects", async () => {
5622+
let t = setup({ routes: REDIRECT_ROUTES });
5623+
5624+
let A = await t.navigate("/parent/child", {
5625+
formMethod: "post",
5626+
formData: createFormData({ key: "value" }),
5627+
});
5628+
// Triggers a GET redirect
5629+
let B = await A.actions.child.redirectReturn("/parent", 301);
5630+
await B.loaders.parent.resolve("PARENT");
5631+
expect(t.router.state).toMatchObject({
5632+
loaderData: {
5633+
parent: "PARENT",
5634+
},
5635+
location: {
5636+
pathname: "/parent",
5637+
},
5638+
navigation: {
5639+
state: "idle",
5640+
},
5641+
});
5642+
});
5643+
5644+
it("should not preserve the method on 302 redirects", async () => {
5645+
let t = setup({ routes: REDIRECT_ROUTES });
5646+
5647+
let A = await t.navigate("/parent/child", {
5648+
formMethod: "post",
5649+
formData: createFormData({ key: "value" }),
5650+
});
5651+
// Triggers a GET redirect
5652+
let B = await A.actions.child.redirectReturn("/parent", 302);
5653+
await B.loaders.parent.resolve("PARENT");
5654+
expect(t.router.state).toMatchObject({
5655+
loaderData: {
5656+
parent: "PARENT",
5657+
},
5658+
location: {
5659+
pathname: "/parent",
5660+
},
5661+
navigation: {
5662+
state: "idle",
5663+
},
5664+
});
5665+
});
5666+
5667+
it("should not preserve the method on 303 redirects", async () => {
5668+
let t = setup({ routes: REDIRECT_ROUTES });
5669+
5670+
let A = await t.navigate("/parent/child", {
5671+
formMethod: "post",
5672+
formData: createFormData({ key: "value" }),
5673+
});
5674+
// Triggers a GET redirect
5675+
let B = await A.actions.child.redirectReturn("/parent", 303);
5676+
await B.loaders.parent.resolve("PARENT");
5677+
expect(t.router.state).toMatchObject({
5678+
loaderData: {
5679+
parent: "PARENT",
5680+
},
5681+
location: {
5682+
pathname: "/parent",
5683+
},
5684+
navigation: {
5685+
state: "idle",
5686+
},
5687+
});
5688+
});
5689+
5690+
it("should not treat 304 as a redirect", async () => {
5691+
let t = setup({ routes: REDIRECT_ROUTES });
5692+
5693+
let A = await t.navigate("/parent");
5694+
await A.loaders.parent.resolve(new Response(null, { status: 304 }));
5695+
expect(t.router.state).toMatchObject({
5696+
loaderData: {},
5697+
location: {
5698+
pathname: "/parent",
5699+
},
5700+
navigation: {
5701+
state: "idle",
5702+
},
5703+
});
5704+
});
5705+
5706+
it("should preserve the method on 307 redirects", async () => {
5707+
let t = setup({ routes: REDIRECT_ROUTES });
5708+
5709+
let A = await t.navigate("/parent/child", {
5710+
formMethod: "post",
5711+
formData: createFormData({ key: "value" }),
5712+
});
5713+
// Triggers a POST redirect
5714+
let B = await A.actions.child.redirectReturn("/parent", 307);
5715+
await B.actions.parent.resolve("PARENT ACTION");
5716+
await B.loaders.parent.resolve("PARENT");
5717+
expect(t.router.state).toMatchObject({
5718+
actionData: {
5719+
parent: "PARENT ACTION",
5720+
},
5721+
loaderData: {
5722+
parent: "PARENT",
5723+
},
5724+
location: {
5725+
pathname: "/parent",
5726+
},
5727+
navigation: {
5728+
state: "idle",
5729+
},
5730+
});
5731+
5732+
let request = B.actions.parent.stub.mock.calls[0][0].request;
5733+
expect(request.method).toBe("POST");
5734+
let fd = await request.formData();
5735+
expect(Array.from(fd.entries())).toEqual([["key", "value"]]);
5736+
});
5737+
5738+
it("should preserve the method on 308 redirects", async () => {
5739+
let t = setup({ routes: REDIRECT_ROUTES });
5740+
5741+
let A = await t.navigate("/parent/child", {
5742+
formMethod: "post",
5743+
formData: createFormData({ key: "value" }),
5744+
});
5745+
// Triggers a POST redirect
5746+
let B = await A.actions.child.redirectReturn("/parent", 308);
5747+
await B.actions.parent.resolve("PARENT ACTION");
5748+
await B.loaders.parent.resolve("PARENT");
5749+
expect(t.router.state).toMatchObject({
5750+
actionData: {
5751+
parent: "PARENT ACTION",
5752+
},
5753+
loaderData: {
5754+
parent: "PARENT",
5755+
},
5756+
location: {
5757+
pathname: "/parent",
5758+
},
5759+
navigation: {
5760+
state: "idle",
5761+
},
5762+
});
5763+
5764+
let request = B.actions.parent.stub.mock.calls[0][0].request;
5765+
expect(request.method).toBe("POST");
5766+
let fd = await request.formData();
5767+
expect(Array.from(fd.entries())).toEqual([["key", "value"]]);
5768+
});
5769+
});
55185770
});
55195771

55205772
describe("scroll restoration", () => {
@@ -6853,7 +7105,7 @@ describe("a router", () => {
68537105
405,
68547106
"Method Not Allowed",
68557107
new Error(
6856-
'You made a post request to "/" but did not provide an `action` ' +
7108+
'You made a POST request to "/" but did not provide an `action` ' +
68577109
'for route "root", so there is no way to handle the request.'
68587110
),
68597111
true

0 commit comments

Comments
 (0)