Skip to content

Commit

Permalink
Hydrate proper error type for subclasses of Error (#10633)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 authored Jun 30, 2023
1 parent 102c599 commit af41cda
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/hydrate-error-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router-dom": patch
---

Support proper hydration of `Error` subclasses such as `ReferenceError`/`TypeError`
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@
"none": "16.2 kB"
},
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
"none": "12.6 kB"
"none": "12.7 kB"
},
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
"none": "18.6 kB"
"none": "18.7 kB"
}
}
}
43 changes: 43 additions & 0 deletions packages/react-router-dom/__tests__/data-browser-router-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,49 @@ function testDomRouter(
`);
});

it("deserializes Error subclass instances from the window", async () => {
window.__staticRouterHydrationData = {
loaderData: {},
actionData: null,
errors: {
"0": {
message: "error message",
__type: "Error",
__subType: "ReferenceError",
},
},
};
let router = createTestRouter(
createRoutesFromElements(
<Route path="/" element={<h1>Nope</h1>} errorElement={<Boundary />} />
)
);
let { container } = render(<RouterProvider router={router} />);

function Boundary() {
let error = useRouteError() as Error;
return error instanceof Error ? (
<>
<pre>{error.toString()}</pre>
<pre>stack:{error.stack}</pre>
</>
) : (
<p>No :(</p>
);
}

expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<pre>
ReferenceError: error message
</pre>
<pre>
stack:
</pre>
</div>"
`);
});

it("renders fallbackElement while first data fetch happens", async () => {
let fooDefer = createDeferred();
let router = createTestRouter(
Expand Down
45 changes: 45 additions & 0 deletions packages/react-router-dom/__tests__/data-static-router-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,51 @@ describe("A <StaticRouterProvider>", () => {
);
});

it("serializes Error subclass instances", async () => {
let routes = [
{
path: "/",
loader: () => {
throw new ReferenceError("oh no");
},
},
];
let { query } = createStaticHandler(routes);

let context = (await query(
new Request("http://localhost/", {
signal: new AbortController().signal,
})
)) as StaticHandlerContext;

let html = ReactDOMServer.renderToStaticMarkup(
<React.StrictMode>
<StaticRouterProvider
router={createStaticRouter(routes, context)}
context={context}
/>
</React.StrictMode>
);

// stack is stripped by default from SSR errors
let expectedJsonString = JSON.stringify(
JSON.stringify({
loaderData: {},
actionData: null,
errors: {
"0": {
message: "oh no",
__type: "Error",
__subType: "ReferenceError",
},
},
})
);
expect(html).toMatch(
`<script>window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`
);
});

it("supports a nonce prop", async () => {
let routes = [
{
Expand Down
29 changes: 24 additions & 5 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,30 @@ function deserializeErrors(
val.internal === true
);
} else if (val && val.__type === "Error") {
let error = new Error(val.message);
// Wipe away the client-side stack trace. Nothing to fill it in with
// because we don't serialize SSR stack traces for security reasons
error.stack = "";
serialized[key] = error;
// Attempt to reconstruct the right type of Error (i.e., ReferenceError)
if (val.__subType) {
let ErrorConstructor = window[val.__subType];
if (typeof ErrorConstructor === "function") {
try {
// @ts-expect-error
let error = new ErrorConstructor(val.message);
// Wipe away the client-side stack trace. Nothing to fill it in with
// because we don't serialize SSR stack traces for security reasons
error.stack = "";
serialized[key] = error;
} catch (e) {
// no-op - fall through and create a normal Error
}
}
}

if (serialized[key] == null) {
let error = new Error(val.message);
// Wipe away the client-side stack trace. Nothing to fill it in with
// because we don't serialize SSR stack traces for security reasons
error.stack = "";
serialized[key] = error;
}
} else {
serialized[key] = val;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/react-router-dom/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,13 @@ function serializeErrors(
serialized[key] = {
message: val.message,
__type: "Error",
// If this is a subclass (i.e., ReferenceError), send up the type so we
// can re-create the same type during hydration.
...(val.name !== "Error"
? {
__subType: val.name,
}
: {}),
};
} else {
serialized[key] = val;
Expand Down

0 comments on commit af41cda

Please sign in to comment.