Skip to content

Commit af41cda

Browse files
authored
Hydrate proper error type for subclasses of Error (#10633)
1 parent 102c599 commit af41cda

File tree

6 files changed

+126
-7
lines changed

6 files changed

+126
-7
lines changed

.changeset/hydrate-error-type.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": patch
3+
---
4+
5+
Support proper hydration of `Error` subclasses such as `ReferenceError`/`TypeError`

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@
118118
"none": "16.2 kB"
119119
},
120120
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
121-
"none": "12.6 kB"
121+
"none": "12.7 kB"
122122
},
123123
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
124-
"none": "18.6 kB"
124+
"none": "18.7 kB"
125125
}
126126
}
127127
}

packages/react-router-dom/__tests__/data-browser-router-test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,49 @@ function testDomRouter(
328328
`);
329329
});
330330

331+
it("deserializes Error subclass instances from the window", async () => {
332+
window.__staticRouterHydrationData = {
333+
loaderData: {},
334+
actionData: null,
335+
errors: {
336+
"0": {
337+
message: "error message",
338+
__type: "Error",
339+
__subType: "ReferenceError",
340+
},
341+
},
342+
};
343+
let router = createTestRouter(
344+
createRoutesFromElements(
345+
<Route path="/" element={<h1>Nope</h1>} errorElement={<Boundary />} />
346+
)
347+
);
348+
let { container } = render(<RouterProvider router={router} />);
349+
350+
function Boundary() {
351+
let error = useRouteError() as Error;
352+
return error instanceof Error ? (
353+
<>
354+
<pre>{error.toString()}</pre>
355+
<pre>stack:{error.stack}</pre>
356+
</>
357+
) : (
358+
<p>No :(</p>
359+
);
360+
}
361+
362+
expect(getHtml(container)).toMatchInlineSnapshot(`
363+
"<div>
364+
<pre>
365+
ReferenceError: error message
366+
</pre>
367+
<pre>
368+
stack:
369+
</pre>
370+
</div>"
371+
`);
372+
});
373+
331374
it("renders fallbackElement while first data fetch happens", async () => {
332375
let fooDefer = createDeferred();
333376
let router = createTestRouter(

packages/react-router-dom/__tests__/data-static-router-test.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,51 @@ describe("A <StaticRouterProvider>", () => {
701701
);
702702
});
703703

704+
it("serializes Error subclass instances", async () => {
705+
let routes = [
706+
{
707+
path: "/",
708+
loader: () => {
709+
throw new ReferenceError("oh no");
710+
},
711+
},
712+
];
713+
let { query } = createStaticHandler(routes);
714+
715+
let context = (await query(
716+
new Request("http://localhost/", {
717+
signal: new AbortController().signal,
718+
})
719+
)) as StaticHandlerContext;
720+
721+
let html = ReactDOMServer.renderToStaticMarkup(
722+
<React.StrictMode>
723+
<StaticRouterProvider
724+
router={createStaticRouter(routes, context)}
725+
context={context}
726+
/>
727+
</React.StrictMode>
728+
);
729+
730+
// stack is stripped by default from SSR errors
731+
let expectedJsonString = JSON.stringify(
732+
JSON.stringify({
733+
loaderData: {},
734+
actionData: null,
735+
errors: {
736+
"0": {
737+
message: "oh no",
738+
__type: "Error",
739+
__subType: "ReferenceError",
740+
},
741+
},
742+
})
743+
);
744+
expect(html).toMatch(
745+
`<script>window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`
746+
);
747+
});
748+
704749
it("supports a nonce prop", async () => {
705750
let routes = [
706751
{

packages/react-router-dom/index.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,30 @@ function deserializeErrors(
278278
val.internal === true
279279
);
280280
} else if (val && val.__type === "Error") {
281-
let error = new Error(val.message);
282-
// Wipe away the client-side stack trace. Nothing to fill it in with
283-
// because we don't serialize SSR stack traces for security reasons
284-
error.stack = "";
285-
serialized[key] = error;
281+
// Attempt to reconstruct the right type of Error (i.e., ReferenceError)
282+
if (val.__subType) {
283+
let ErrorConstructor = window[val.__subType];
284+
if (typeof ErrorConstructor === "function") {
285+
try {
286+
// @ts-expect-error
287+
let error = new ErrorConstructor(val.message);
288+
// Wipe away the client-side stack trace. Nothing to fill it in with
289+
// because we don't serialize SSR stack traces for security reasons
290+
error.stack = "";
291+
serialized[key] = error;
292+
} catch (e) {
293+
// no-op - fall through and create a normal Error
294+
}
295+
}
296+
}
297+
298+
if (serialized[key] == null) {
299+
let error = new Error(val.message);
300+
// Wipe away the client-side stack trace. Nothing to fill it in with
301+
// because we don't serialize SSR stack traces for security reasons
302+
error.stack = "";
303+
serialized[key] = error;
304+
}
286305
} else {
287306
serialized[key] = val;
288307
}

packages/react-router-dom/server.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,13 @@ function serializeErrors(
179179
serialized[key] = {
180180
message: val.message,
181181
__type: "Error",
182+
// If this is a subclass (i.e., ReferenceError), send up the type so we
183+
// can re-create the same type during hydration.
184+
...(val.name !== "Error"
185+
? {
186+
__subType: val.name,
187+
}
188+
: {}),
182189
};
183190
} else {
184191
serialized[key] = val;

0 commit comments

Comments
 (0)