Skip to content

Commit 0685cd9

Browse files
authored
Add proper 404 error for missing loader (#10345)
1 parent 530a504 commit 0685cd9

File tree

3 files changed

+170
-10
lines changed

3 files changed

+170
-10
lines changed

.changeset/fetcher-404.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+
Ensure proper 404 error on `fetcher.load` call to a route without a `loader`

packages/router/__tests__/router-test.ts

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10280,6 +10280,150 @@ describe("a router", () => {
1028010280
await F.actions.index.resolve("INDEX ACTION");
1028110281
expect(t.router.getFetcher(key).data).toBe("INDEX ACTION");
1028210282
});
10283+
10284+
it("throws a 404 ErrorResponse without ?index and parent route has no loader", async () => {
10285+
let t = setup({
10286+
routes: [
10287+
{
10288+
id: "parent",
10289+
path: "parent",
10290+
children: [
10291+
{
10292+
id: "index",
10293+
index: true,
10294+
loader: true,
10295+
},
10296+
],
10297+
},
10298+
],
10299+
initialEntries: ["/parent"],
10300+
hydrationData: { loaderData: { index: "INDEX" } },
10301+
});
10302+
10303+
let key = "KEY";
10304+
await t.fetch("/parent");
10305+
expect(t.router.state.errors).toMatchInlineSnapshot(`
10306+
{
10307+
"parent": ErrorResponse {
10308+
"data": "Error: No route matches URL "/parent"",
10309+
"error": [Error: No route matches URL "/parent"],
10310+
"internal": true,
10311+
"status": 404,
10312+
"statusText": "Not Found",
10313+
},
10314+
}
10315+
`);
10316+
expect(t.router.getFetcher(key).data).toBe(undefined);
10317+
});
10318+
10319+
it("throws a 404 ErrorResponse with ?index and index route has no loader", async () => {
10320+
let t = setup({
10321+
routes: [
10322+
{
10323+
id: "parent",
10324+
path: "parent",
10325+
loader: true,
10326+
children: [
10327+
{
10328+
id: "index",
10329+
index: true,
10330+
},
10331+
],
10332+
},
10333+
],
10334+
initialEntries: ["/parent"],
10335+
hydrationData: { loaderData: { parent: "PARENT" } },
10336+
});
10337+
10338+
let key = "KEY";
10339+
await t.fetch("/parent?index");
10340+
expect(t.router.state.errors).toMatchInlineSnapshot(`
10341+
{
10342+
"parent": ErrorResponse {
10343+
"data": "Error: No route matches URL "/parent?index"",
10344+
"error": [Error: No route matches URL "/parent?index"],
10345+
"internal": true,
10346+
"status": 404,
10347+
"statusText": "Not Found",
10348+
},
10349+
}
10350+
`);
10351+
expect(t.router.getFetcher(key).data).toBe(undefined);
10352+
});
10353+
10354+
it("throws a 405 ErrorResponse without ?index and parent route has no action", async () => {
10355+
let t = setup({
10356+
routes: [
10357+
{
10358+
id: "parent",
10359+
path: "parent",
10360+
children: [
10361+
{
10362+
id: "index",
10363+
index: true,
10364+
action: true,
10365+
},
10366+
],
10367+
},
10368+
],
10369+
initialEntries: ["/parent"],
10370+
});
10371+
10372+
let key = "KEY";
10373+
await t.fetch("/parent", {
10374+
formMethod: "post",
10375+
formData: createFormData({}),
10376+
});
10377+
expect(t.router.state.errors).toMatchInlineSnapshot(`
10378+
{
10379+
"parent": ErrorResponse {
10380+
"data": "Error: You made a POST request to "/parent" but did not provide an \`action\` for route "parent", so there is no way to handle the request.",
10381+
"error": [Error: You made a POST request to "/parent" but did not provide an \`action\` for route "parent", so there is no way to handle the request.],
10382+
"internal": true,
10383+
"status": 405,
10384+
"statusText": "Method Not Allowed",
10385+
},
10386+
}
10387+
`);
10388+
expect(t.router.getFetcher(key).data).toBe(undefined);
10389+
});
10390+
10391+
it("throws a 405 ErrorResponse with ?index and index route has no action", async () => {
10392+
let t = setup({
10393+
routes: [
10394+
{
10395+
id: "parent",
10396+
path: "parent",
10397+
action: true,
10398+
children: [
10399+
{
10400+
id: "index",
10401+
index: true,
10402+
},
10403+
],
10404+
},
10405+
],
10406+
initialEntries: ["/parent"],
10407+
});
10408+
10409+
let key = "KEY";
10410+
await t.fetch("/parent?index", {
10411+
formMethod: "post",
10412+
formData: createFormData({}),
10413+
});
10414+
expect(t.router.state.errors).toMatchInlineSnapshot(`
10415+
{
10416+
"parent": ErrorResponse {
10417+
"data": "Error: You made a POST request to "/parent?index" but did not provide an \`action\` for route "parent", so there is no way to handle the request.",
10418+
"error": [Error: You made a POST request to "/parent?index" but did not provide an \`action\` for route "parent", so there is no way to handle the request.],
10419+
"internal": true,
10420+
"status": 405,
10421+
"statusText": "Method Not Allowed",
10422+
},
10423+
}
10424+
`);
10425+
expect(t.router.getFetcher(key).data).toBe(undefined);
10426+
});
1028310427
});
1028410428
});
1028510429

@@ -15444,12 +15588,20 @@ describe("a router", () => {
1544415588
expect(currentRouter.state.loaderData).toEqual({
1544515589
root: "ROOT*",
1544615590
});
15447-
// Fetcher should have been revalidated but thrown an errow since the
15591+
// Fetcher should have been revalidated but throw an error since the
1544815592
// loader was removed
1544915593
expect(currentRouter.state.fetchers.get("key")?.data).toBe(undefined);
15450-
expect(currentRouter.state.errors).toEqual({
15451-
root: new Error('Could not find the loader to run on the "foo" route'),
15452-
});
15594+
expect(currentRouter.state.errors).toMatchInlineSnapshot(`
15595+
{
15596+
"root": ErrorResponse {
15597+
"data": "Error: No route matches URL "/foo"",
15598+
"error": [Error: No route matches URL "/foo"],
15599+
"internal": true,
15600+
"status": 404,
15601+
"statusText": "Not Found",
15602+
},
15603+
}
15604+
`);
1545315605
});
1545415606

1545515607
it("should retain existing routes until revalidation completes on route removal (fetch)", async () => {

packages/router/router.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3429,9 +3429,11 @@ async function callLoaderOrAction(
34293429
// previously-lazy-loaded routes
34303430
result = await runHandler(handler);
34313431
} else if (type === "action") {
3432+
let url = new URL(request.url);
3433+
let pathname = url.pathname + url.search;
34323434
throw getInternalRouterError(405, {
34333435
method: request.method,
3434-
pathname: new URL(request.url).pathname,
3436+
pathname,
34353437
routeId: match.route.id,
34363438
});
34373439
} else {
@@ -3440,12 +3442,13 @@ async function callLoaderOrAction(
34403442
return { type: ResultType.data, data: undefined };
34413443
}
34423444
}
3445+
} else if (!handler) {
3446+
let url = new URL(request.url);
3447+
let pathname = url.pathname + url.search;
3448+
throw getInternalRouterError(404, {
3449+
pathname,
3450+
});
34433451
} else {
3444-
invariant<Function>(
3445-
handler,
3446-
`Could not find the ${type} to run on the "${match.route.id}" route`
3447-
);
3448-
34493452
result = await runHandler(handler);
34503453
}
34513454

0 commit comments

Comments
 (0)