Skip to content

Commit 9d22943

Browse files
authored
Use turbo stream for framework hydration errors (#15175)
* Use turbo stream for framework hydration errors * Avoid serializing internal error response flag
1 parent bf63729 commit 9d22943

15 files changed

Lines changed: 111 additions & 97 deletions

File tree

integration/error-sanitization-test.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -179,11 +179,8 @@ test.describe("Error Sanitization", () => {
179179
expect(html).toMatch("Index Error");
180180
expect(html).not.toMatch("LOADER");
181181
expect(html).toMatch("MESSAGE:Unexpected Server Error");
182-
// This is the turbo-stream encoding - the fact that stack goes right
183-
// into __type means it has no value
184-
expect(html).toMatch(
185-
'\\"message\\",\\"Unexpected Server Error\\",\\"stack\\",\\"__type\\",\\"Error\\"',
186-
);
182+
expect(html).toMatch('\\"SanitizedError\\"');
183+
expect(html).toMatch('\\"Error\\",\\"Unexpected Server Error\\"');
187184
expect(html).not.toMatch(/ at /i);
188185
expect(errorLogs.length).toBe(1);
189186
expect(errorLogs[0][0].message).toMatch("Loader Error");
@@ -195,11 +192,8 @@ test.describe("Error Sanitization", () => {
195192
let html = await response.text();
196193
expect(html).toMatch("Index Error");
197194
expect(html).toMatch("MESSAGE:Unexpected Server Error");
198-
// This is the turbo-stream encoding - the fact that stack goes right
199-
// into __type means it has no value
200-
expect(html).toMatch(
201-
'\\"message\\",\\"Unexpected Server Error\\",\\"stack\\",\\"__type\\",\\"Error\\"',
202-
);
195+
expect(html).toMatch('\\"SanitizedError\\"');
196+
expect(html).toMatch('\\"Error\\",\\"Unexpected Server Error\\"');
203197
expect(html).not.toMatch(/ at /i);
204198
expect(errorLogs.length).toBe(1);
205199
expect(errorLogs[0][0].message).toMatch("Render Error");
@@ -571,11 +565,8 @@ test.describe("Error Sanitization", () => {
571565
expect(html).toMatch("Index Error");
572566
expect(html).not.toMatch("LOADER");
573567
expect(html).toMatch("MESSAGE:Unexpected Server Error");
574-
// This is the turbo-stream encoding - the fact that stack goes right
575-
// into __type means it has no value
576-
expect(html).toMatch(
577-
'\\"message\\",\\"Unexpected Server Error\\",\\"stack\\",\\"__type\\",\\"Error\\"',
578-
);
568+
expect(html).toMatch('\\"SanitizedError\\"');
569+
expect(html).toMatch('\\"Error\\",\\"Unexpected Server Error\\"');
579570
expect(html).not.toMatch(/ at /i);
580571
expect(errorLogs[0][0]).toEqual("App Specific Error Logging:");
581572
expect(errorLogs[1][0]).toEqual(" Request: GET test://test/?loader");
@@ -589,11 +580,8 @@ test.describe("Error Sanitization", () => {
589580
let html = await response.text();
590581
expect(html).toMatch("Index Error");
591582
expect(html).toMatch("MESSAGE:Unexpected Server Error");
592-
// This is the turbo-stream encoding - the fact that stack goes right
593-
// into __type means it has no value
594-
expect(html).toMatch(
595-
'\\"message\\",\\"Unexpected Server Error\\",\\"stack\\",\\"__type\\",\\"Error\\"',
596-
);
583+
expect(html).toMatch('\\"SanitizedError\\"');
584+
expect(html).toMatch('\\"Error\\",\\"Unexpected Server Error\\"');
597585
expect(html).not.toMatch(/ at /i);
598586
expect(errorLogs[0][0]).toEqual("App Specific Error Logging:");
599587
expect(errorLogs[1][0]).toEqual(" Request: GET test://test/?render");
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Use `turbo-stream` to serialize and deserialize Framework Mode hydration errors
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Remove the un-documented custom error serialization logic from Data Mode SSR built-in hydration flows

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8103,6 +8103,56 @@ function testDomRouter(
81038103
`);
81048104
});
81058105

8106+
it("does not deserialize custom Error subclass instances from the window", () => {
8107+
try {
8108+
(window as any).CustomError = class CustomError extends Error {};
8109+
window.__staticRouterHydrationData = {
8110+
loaderData: {},
8111+
actionData: null,
8112+
errors: {
8113+
"0": {
8114+
message: "custom error message",
8115+
__type: "Error",
8116+
__subType: "CustomError",
8117+
},
8118+
},
8119+
};
8120+
let router = createTestRouter([
8121+
{
8122+
path: "/",
8123+
Component: () => <h1>Nope</h1>,
8124+
ErrorBoundary: () => <Boundary />,
8125+
},
8126+
]);
8127+
let { container } = render(<RouterProvider router={router} />);
8128+
8129+
function Boundary() {
8130+
let error = useRouteError() as Error;
8131+
return error instanceof Error ? (
8132+
<>
8133+
<pre>{error.constructor.name}</pre>
8134+
<pre>{error.toString()}</pre>
8135+
</>
8136+
) : (
8137+
<p>No :(</p>
8138+
);
8139+
}
8140+
8141+
expect(getHtml(container)).toMatchInlineSnapshot(`
8142+
"<div>
8143+
<pre>
8144+
Error
8145+
</pre>
8146+
<pre>
8147+
Error: custom error message
8148+
</pre>
8149+
</div>"
8150+
`);
8151+
} finally {
8152+
delete (window as any).CustomError;
8153+
}
8154+
});
8155+
81068156
it("renders hydration errors on leaf elements", async () => {
81078157
let router = createTestRouter(
81088158
[

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { decodeViaTurboStream } from "../../lib/dom/ssr/single-fetch";
2+
import {
3+
ErrorResponseImpl,
4+
isRouteErrorResponse,
5+
} from "../../lib/router/utils";
26
import { createRequestHandler } from "../../lib/server-runtime/server";
7+
import { encodeViaTurboStream } from "../../lib/server-runtime/single-fetch";
8+
import { ServerMode } from "../../lib/server-runtime/mode";
39
import { mockServerBuild } from "./utils";
410

511
describe("loaders", () => {
@@ -33,3 +39,27 @@ describe("loaders", () => {
3339
expect((decoded.value as any)[routeId].data).toEqual("/random");
3440
});
3541
});
42+
43+
describe("turbo-stream error decoding", () => {
44+
it("decodes ErrorResponse instances", async () => {
45+
let body = encodeViaTurboStream(
46+
{
47+
errors: {
48+
root: new ErrorResponseImpl(404, "Not Found", "Missing", true),
49+
},
50+
},
51+
new AbortController().signal,
52+
undefined,
53+
ServerMode.Development,
54+
);
55+
56+
let decoded = await decodeViaTurboStream(body, global);
57+
let error = (decoded.value as any).errors.root;
58+
59+
expect(isRouteErrorResponse(error)).toBe(true);
60+
expect(error.status).toBe(404);
61+
expect(error.statusText).toBe("Not Found");
62+
expect(error.data).toBe("Missing");
63+
expect(error.internal).toBe(false);
64+
});
65+
});

packages/react-router/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -395,9 +395,6 @@ export { FrameworkContext as UNSAFE_FrameworkContext } from "./lib/dom/ssr/compo
395395
/** @internal */
396396
export type { AssetsManifest as UNSAFE_AssetsManifest } from "./lib/dom/ssr/entry";
397397

398-
/** @internal */
399-
export { deserializeErrors as UNSAFE_deserializeErrors } from "./lib/dom/ssr/errors";
400-
401398
/** @internal */
402399
export { RemixErrorBoundary as UNSAFE_RemixErrorBoundary } from "./lib/dom/ssr/errorBoundaries";
403400

packages/react-router/lib/dom-export/hydrated-router.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
UNSAFE_createBrowserHistory as createBrowserHistory,
1818
UNSAFE_createClientRoutes as createClientRoutes,
1919
UNSAFE_createRouter as createRouter,
20-
UNSAFE_deserializeErrors as deserializeErrors,
2120
UNSAFE_getTurboStreamSingleFetchDataStrategy as getTurboStreamSingleFetchDataStrategy,
2221
UNSAFE_getPatchRoutesOnNavigationFunction as getPatchRoutesOnNavigationFunction,
2322
UNSAFE_useFogOFWarDiscovery as useFogOFWarDiscovery,
@@ -159,11 +158,6 @@ function createHydratedRouter({
159158
isSpaMode: ssrInfo.context.isSpaMode,
160159
});
161160

162-
if (hydrationData && hydrationData.errors) {
163-
// TODO: De-dup this or remove entirely in v7 where single fetch is the
164-
// only approach and we have already serialized or deserialized on the server
165-
hydrationData.errors = deserializeErrors(hydrationData.errors);
166-
}
167161
}
168162

169163
// We cannot support history-state-driven masking with SSR, so if a hard

packages/react-router/lib/dom/lib.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import type {
3636
} from "../router/utils";
3737
import {
3838
ErrorResponseImpl,
39+
SUPPORTED_ERROR_TYPES,
3940
joinPaths,
4041
matchPath,
4142
parseToInfo,
@@ -740,7 +741,10 @@ function deserializeErrors(
740741
);
741742
} else if (val && val.__type === "Error") {
742743
// Attempt to reconstruct the right type of Error (i.e., ReferenceError)
743-
if (val.__subType) {
744+
if (
745+
typeof val.__subType === "string" &&
746+
SUPPORTED_ERROR_TYPES.includes(val.__subType)
747+
) {
744748
let ErrorConstructor = window[val.__subType];
745749
if (typeof ErrorConstructor === "function") {
746750
try {

packages/react-router/lib/dom/ssr/errors.ts

Lines changed: 0 additions & 49 deletions
This file was deleted.

packages/react-router/lib/dom/ssr/single-fetch.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import * as React from "react";
22

3-
import {
4-
SUPPORTED_ERROR_TYPES,
5-
decode,
6-
} from "../../../vendor/turbo-stream-v2/turbo-stream";
3+
import { decode } from "../../../vendor/turbo-stream-v2/turbo-stream";
74
import type { Router as DataRouter } from "../../router/router";
85
import { isDataWithResponseInit, isResponse } from "../../router/router";
96
import type {
@@ -14,6 +11,7 @@ import type {
1411
} from "../../router/utils";
1512
import {
1613
ErrorResponseImpl,
14+
SUPPORTED_ERROR_TYPES,
1715
isRouteErrorResponse,
1816
redirect,
1917
data,

0 commit comments

Comments
 (0)