Skip to content

Commit b76ed3d

Browse files
fix(rsc): handle ErrorResponse in resource routes (#13966)
* fix(rsc): handle `ErrorResponse` in resource routes * avoid toString, pass route error response to onError
1 parent b3337fc commit b76ed3d

File tree

2 files changed

+117
-2
lines changed

2 files changed

+117
-2
lines changed

integration/rsc/rsc-test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,115 @@ implementations.forEach((implementation) => {
935935
});
936936
});
937937

938+
test("Handles error responses from resource routes missing loaders/actions", async ({
939+
page,
940+
request,
941+
}) => {
942+
let port = await getPort();
943+
stop = await setupRscTest({
944+
implementation,
945+
port,
946+
files: {
947+
"src/routes.ts": js`
948+
import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";
949+
950+
export const routes = [
951+
{
952+
id: "root",
953+
path: "",
954+
lazy: () => import("./routes/root"),
955+
children: [
956+
{
957+
id: "home",
958+
index: true,
959+
lazy: () => import("./routes/home"),
960+
},
961+
],
962+
},
963+
{
964+
id: "no-loader-resource",
965+
path: "no-loader-resource",
966+
lazy: () => import("./routes/no-loader-resource"),
967+
},
968+
{
969+
id: "no-action-resource",
970+
path: "no-action-resource",
971+
lazy: () => import("./routes/no-action-resource"),
972+
},
973+
] satisfies RSCRouteConfig;
974+
`,
975+
"src/routes/root.tsx": js`
976+
import { Outlet } from "react-router";
977+
export default function RootRoute() {
978+
return (
979+
<div>
980+
<h1>Root Route</h1>
981+
<Outlet />
982+
</div>
983+
);
984+
}
985+
`,
986+
"src/routes/home.tsx": js`
987+
export default function HomeRoute() {
988+
return (
989+
<div>
990+
<h2 data-home>Home Route</h2>
991+
</div>
992+
);
993+
}
994+
`,
995+
"src/routes/no-loader-resource.tsx": js`
996+
// This resource route has no loader, so GET requests should fail
997+
export async function action() {
998+
return { message: "no-loader-resource action works" };
999+
}
1000+
`,
1001+
"src/routes/no-action-resource.tsx": js`
1002+
// This resource route has no action, so POST requests should fail
1003+
export function loader() {
1004+
return { message: "no-action-resource loader works" };
1005+
}
1006+
`,
1007+
},
1008+
});
1009+
1010+
const getResponse = await request.get(
1011+
`http://localhost:${port}/no-loader-resource`
1012+
);
1013+
expect(getResponse?.status()).toBe(400);
1014+
expect(await getResponse?.text()).toBe(
1015+
'Error: You made a GET request to "/no-loader-resource" but did not provide a `loader` for route "no-loader-resource", so there is no way to handle the request.'
1016+
);
1017+
1018+
const postResponse = await request.post(
1019+
`http://localhost:${port}/no-action-resource`
1020+
);
1021+
expect(postResponse?.status()).toBe(405);
1022+
expect(await postResponse?.text()).toBe(
1023+
'Error: You made a POST request to "/no-action-resource" but did not provide an `action` for route "no-action-resource", so there is no way to handle the request.'
1024+
);
1025+
1026+
const postWithActionResponse = await request.post(
1027+
`http://localhost:${port}/no-loader-resource`
1028+
);
1029+
expect(postWithActionResponse?.status()).toBe(200);
1030+
expect(await postWithActionResponse?.json()).toEqual({
1031+
message: "no-loader-resource action works",
1032+
});
1033+
1034+
const getWithLoaderResponse = await request.get(
1035+
`http://localhost:${port}/no-action-resource`
1036+
);
1037+
expect(getWithLoaderResponse?.status()).toBe(200);
1038+
expect(await getWithLoaderResponse?.json()).toEqual({
1039+
message: "no-action-resource loader works",
1040+
});
1041+
1042+
// Ensure this is using RSC
1043+
await page.goto(`http://localhost:${port}/`);
1044+
validateRSCHtml(await page.content());
1045+
});
1046+
9381047
test.describe("Server Actions", () => {
9391048
test("Supports React Server Functions", async ({ page }) => {
9401049
let port = await getPort();

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,10 +535,16 @@ async function generateResourceResponse(
535535
} catch (error) {
536536
if (isResponse(error)) {
537537
result = error;
538+
} else if (isRouteErrorResponse(error)) {
539+
onError?.(error);
540+
const errorMessage =
541+
typeof error.data === "string" ? error.data : error.statusText;
542+
result = new Response(errorMessage, {
543+
status: error.status,
544+
statusText: error.statusText,
545+
});
538546
} else {
539-
// TODO: Do we need to handle ErrorResponse?
540547
onError?.(error);
541-
542548
result = new Response("Internal Server Error", {
543549
status: 500,
544550
});

0 commit comments

Comments
 (0)