Skip to content

Commit 6e98814

Browse files
authored
fix(react-router): Add better error messaging when getLoadContext isn't updated (#13242)
1 parent b8cf1b6 commit 6e98814

File tree

3 files changed

+256
-37
lines changed

3 files changed

+256
-37
lines changed

.changeset/sixty-tigers-poke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
UNSTABLE: Add better error messaging when `getLoadContext` is not updated to return a `Map`"

packages/react-router/__tests__/server-runtime/server-test.ts

Lines changed: 213 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
* @jest-environment node
33
*/
44

5-
import type { StaticHandlerContext } from "react-router";
5+
import {
6+
unstable_createContext,
7+
type StaticHandlerContext,
8+
} from "react-router";
69

710
import { createRequestHandler } from "../../lib/server-runtime/server";
811
import { ServerMode } from "../../lib/server-runtime/mode";
@@ -24,7 +27,7 @@ function spyConsole() {
2427
return spy;
2528
}
2629

27-
describe.skip("server", () => {
30+
describe("server", () => {
2831
let routeId = "root";
2932
let build: ServerBuild = {
3033
ssr: true,
@@ -72,20 +75,20 @@ describe.skip("server", () => {
7275
});
7376

7477
let allowThrough = [
75-
["GET", "/"],
76-
["GET", "/?_data=root"],
77-
["POST", "/"],
78-
["POST", "/?_data=root"],
79-
["PUT", "/"],
80-
["PUT", "/?_data=root"],
81-
["DELETE", "/"],
82-
["DELETE", "/?_data=root"],
83-
["PATCH", "/"],
84-
["PATCH", "/?_data=root"],
78+
["GET", "/", "COMPONENT"],
79+
["GET", "/_root.data", "LOADER"],
80+
["POST", "/", "COMPONENT"],
81+
["POST", "/_root.data", "ACTION"],
82+
["PUT", "/", "COMPONENT"],
83+
["PUT", "/_root.data", "ACTION"],
84+
["DELETE", "/", "COMPONENT"],
85+
["DELETE", "/_root.data", "ACTION"],
86+
["PATCH", "/", "COMPONENT"],
87+
["PATCH", "/_root.data", "ACTION"],
8588
];
8689
it.each(allowThrough)(
8790
`allows through %s request to %s`,
88-
async (method, to) => {
91+
async (method, to, expected) => {
8992
let handler = createRequestHandler(build);
9093
let response = await handler(
9194
new Request(`http://localhost:3000${to}`, {
@@ -96,11 +99,6 @@ describe.skip("server", () => {
9699
expect(response.status).toBe(200);
97100
let text = await response.text();
98101
expect(text).toContain(method);
99-
let expected = !to.includes("?_data=root")
100-
? "COMPONENT"
101-
: method === "GET"
102-
? "LOADER"
103-
: "ACTION";
104102
expect(text).toContain(expected);
105103
expect(spy.console).not.toHaveBeenCalled();
106104
}
@@ -116,6 +114,203 @@ describe.skip("server", () => {
116114

117115
expect(await response.text()).toBe("");
118116
});
117+
118+
it("accepts proper values from getLoadContext (without middleware)", async () => {
119+
let handler = createRequestHandler({
120+
ssr: true,
121+
entry: {
122+
module: {
123+
default: async (request) => {
124+
return new Response(
125+
`${request.method}, ${request.url} COMPONENT`
126+
);
127+
},
128+
},
129+
},
130+
routes: {
131+
root: {
132+
id: "root",
133+
path: "",
134+
module: {
135+
loader: ({ context }) => context.foo,
136+
default: () => "COMPONENT",
137+
},
138+
},
139+
},
140+
assets: {
141+
routes: {
142+
root: {
143+
clientActionModule: undefined,
144+
clientLoaderModule: undefined,
145+
clientMiddlewareModule: undefined,
146+
hasAction: true,
147+
hasClientAction: false,
148+
hasClientLoader: false,
149+
hasClientMiddleware: false,
150+
hasErrorBoundary: false,
151+
hasLoader: true,
152+
hydrateFallbackModule: undefined,
153+
id: routeId,
154+
module: routeId,
155+
path: "",
156+
},
157+
},
158+
entry: { imports: [], module: "" },
159+
url: "",
160+
version: "",
161+
},
162+
future: {
163+
unstable_middleware: false,
164+
},
165+
prerender: [],
166+
publicPath: "/",
167+
assetsBuildDirectory: "/",
168+
isSpaMode: false,
169+
});
170+
let response = await handler(
171+
new Request("http://localhost:3000/_root.data"),
172+
{
173+
foo: "FOO",
174+
}
175+
);
176+
177+
expect(await response.text()).toContain("FOO");
178+
});
179+
180+
it("accepts proper values from getLoadContext (with middleware)", async () => {
181+
let fooContext = unstable_createContext<string>();
182+
let handler = createRequestHandler({
183+
ssr: true,
184+
entry: {
185+
module: {
186+
default: async (request) => {
187+
return new Response(
188+
`${request.method}, ${request.url} COMPONENT`
189+
);
190+
},
191+
},
192+
},
193+
routes: {
194+
root: {
195+
id: "root",
196+
path: "",
197+
module: {
198+
loader: ({ context }) => context.get(fooContext),
199+
default: () => "COMPONENT",
200+
},
201+
},
202+
},
203+
assets: {
204+
routes: {
205+
root: {
206+
clientActionModule: undefined,
207+
clientLoaderModule: undefined,
208+
clientMiddlewareModule: undefined,
209+
hasAction: true,
210+
hasClientAction: false,
211+
hasClientLoader: false,
212+
hasClientMiddleware: false,
213+
hasErrorBoundary: false,
214+
hasLoader: true,
215+
hydrateFallbackModule: undefined,
216+
id: routeId,
217+
module: routeId,
218+
path: "",
219+
},
220+
},
221+
entry: { imports: [], module: "" },
222+
url: "",
223+
version: "",
224+
},
225+
future: {
226+
unstable_middleware: true,
227+
},
228+
prerender: [],
229+
publicPath: "/",
230+
assetsBuildDirectory: "/",
231+
isSpaMode: false,
232+
});
233+
let response = await handler(
234+
new Request("http://localhost:3000/_root.data"),
235+
// @ts-expect-error In apps the expected type is handled via the Future interface
236+
new Map([[fooContext, "FOO"]])
237+
);
238+
239+
expect(await response.text()).toContain("FOO");
240+
});
241+
242+
it("errors if an invalid value is returned from getLoadContext (with middleware)", async () => {
243+
let handleErrorSpy = jest.fn();
244+
let handler = createRequestHandler({
245+
ssr: true,
246+
entry: {
247+
module: {
248+
handleError: handleErrorSpy,
249+
default: async (request) => {
250+
return new Response(
251+
`${request.method}, ${request.url} COMPONENT`
252+
);
253+
},
254+
},
255+
},
256+
routes: {
257+
root: {
258+
id: "root",
259+
path: "",
260+
module: {
261+
loader: ({ context }) => context.foo,
262+
default: () => "COMPONENT",
263+
},
264+
},
265+
},
266+
assets: {
267+
routes: {
268+
root: {
269+
clientActionModule: undefined,
270+
clientLoaderModule: undefined,
271+
clientMiddlewareModule: undefined,
272+
hasAction: true,
273+
hasClientAction: false,
274+
hasClientLoader: false,
275+
hasClientMiddleware: false,
276+
hasErrorBoundary: false,
277+
hasLoader: true,
278+
hydrateFallbackModule: undefined,
279+
id: routeId,
280+
module: routeId,
281+
path: "",
282+
},
283+
},
284+
entry: { imports: [], module: "" },
285+
url: "",
286+
version: "",
287+
},
288+
future: {
289+
unstable_middleware: true,
290+
},
291+
prerender: [],
292+
publicPath: "/",
293+
assetsBuildDirectory: "/",
294+
isSpaMode: false,
295+
});
296+
297+
let response = await handler(
298+
new Request("http://localhost:3000/_root.data"),
299+
{
300+
foo: "FOO",
301+
}
302+
);
303+
304+
expect(response.status).toBe(500);
305+
expect(await response.text()).toContain("Unexpected Server Error");
306+
expect(handleErrorSpy).toHaveBeenCalledTimes(1);
307+
expect(handleErrorSpy.mock.calls[0][0]).toMatchInlineSnapshot(`
308+
[Error: Unable to create initial \`unstable_RouterContextProvider\` instance. Please confirm you are returning an instance of \`Map<unstable_routerContext, unknown>\` from your \`getLoadContext\` function.
309+
310+
Error: TypeError: init is not iterable]
311+
`);
312+
handleErrorSpy.mockRestore();
313+
});
119314
});
120315
});
121316

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

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,6 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
9090
return async function requestHandler(request, initialContext) {
9191
_build = typeof build === "function" ? await build() : build;
9292

93-
let loadContext = _build.future.unstable_middleware
94-
? new unstable_RouterContextProvider(
95-
initialContext as unknown as unstable_InitialContext
96-
)
97-
: initialContext || {};
98-
9993
if (typeof build === "function") {
10094
let derived = derive(_build, mode);
10195
routes = derived.routes;
@@ -110,6 +104,44 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
110104
errorHandler = derived.errorHandler;
111105
}
112106

107+
let params: RouteMatch<ServerRoute>["params"] = {};
108+
let loadContext: AppLoadContext | unstable_RouterContextProvider;
109+
110+
let handleError = (error: unknown) => {
111+
if (mode === ServerMode.Development) {
112+
getDevServerHooks()?.processRequestError?.(error);
113+
}
114+
115+
errorHandler(error, {
116+
context: loadContext,
117+
params,
118+
request,
119+
});
120+
};
121+
122+
if (_build.future.unstable_middleware) {
123+
if (initialContext == null) {
124+
loadContext = new unstable_RouterContextProvider();
125+
} else {
126+
try {
127+
loadContext = new unstable_RouterContextProvider(
128+
initialContext as unknown as unstable_InitialContext
129+
);
130+
} catch (e) {
131+
let error = new Error(
132+
"Unable to create initial `unstable_RouterContextProvider` instance. " +
133+
"Please confirm you are returning an instance of " +
134+
"`Map<unstable_routerContext, unknown>` from your `getLoadContext` function." +
135+
`\n\nError: ${e instanceof Error ? e.toString() : e}`
136+
);
137+
handleError(error);
138+
return returnLastResortErrorResponse(error, serverMode);
139+
}
140+
}
141+
} else {
142+
loadContext = initialContext || {};
143+
}
144+
113145
let url = new URL(request.url);
114146

115147
let normalizedBasename = _build.basename || "/";
@@ -127,19 +159,6 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
127159
normalizedPath = normalizedPath.slice(0, -1);
128160
}
129161

130-
let params: RouteMatch<ServerRoute>["params"] = {};
131-
let handleError = (error: unknown) => {
132-
if (mode === ServerMode.Development) {
133-
getDevServerHooks()?.processRequestError?.(error);
134-
}
135-
136-
errorHandler(error, {
137-
context: loadContext,
138-
params,
139-
request,
140-
});
141-
};
142-
143162
// When runtime SSR is disabled, make our dev server behave like the deployed
144163
// pre-rendered site would
145164
if (!_build.ssr) {

0 commit comments

Comments
 (0)