+
+ Error
+
+
+ Error: custom error message
+
+
"
+ `);
+ } finally {
+ delete (window as any).CustomError;
+ }
+ });
+
it("renders hydration errors on leaf elements", async () => {
let router = createTestRouter(
[
diff --git a/packages/react-router/__tests__/server-runtime/data-test.ts b/packages/react-router/__tests__/server-runtime/data-test.ts
index bf196b458f..2eba5adefe 100644
--- a/packages/react-router/__tests__/server-runtime/data-test.ts
+++ b/packages/react-router/__tests__/server-runtime/data-test.ts
@@ -1,5 +1,11 @@
import { decodeViaTurboStream } from "../../lib/dom/ssr/single-fetch";
+import {
+ ErrorResponseImpl,
+ isRouteErrorResponse,
+} from "../../lib/router/utils";
import { createRequestHandler } from "../../lib/server-runtime/server";
+import { encodeViaTurboStream } from "../../lib/server-runtime/single-fetch";
+import { ServerMode } from "../../lib/server-runtime/mode";
import { mockServerBuild } from "./utils";
describe("loaders", () => {
@@ -33,3 +39,27 @@ describe("loaders", () => {
expect((decoded.value as any)[routeId].data).toEqual("/random");
});
});
+
+describe("turbo-stream error decoding", () => {
+ it("decodes ErrorResponse instances", async () => {
+ let body = encodeViaTurboStream(
+ {
+ errors: {
+ root: new ErrorResponseImpl(404, "Not Found", "Missing", true),
+ },
+ },
+ new AbortController().signal,
+ undefined,
+ ServerMode.Development,
+ );
+
+ let decoded = await decodeViaTurboStream(body, global);
+ let error = (decoded.value as any).errors.root;
+
+ expect(isRouteErrorResponse(error)).toBe(true);
+ expect(error.status).toBe(404);
+ expect(error.statusText).toBe("Not Found");
+ expect(error.data).toBe("Missing");
+ expect(error.internal).toBe(false);
+ });
+});
diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts
index 681ecf7ec4..b49c3e8d32 100644
--- a/packages/react-router/index.ts
+++ b/packages/react-router/index.ts
@@ -395,9 +395,6 @@ export { FrameworkContext as UNSAFE_FrameworkContext } from "./lib/dom/ssr/compo
/** @internal */
export type { AssetsManifest as UNSAFE_AssetsManifest } from "./lib/dom/ssr/entry";
-/** @internal */
-export { deserializeErrors as UNSAFE_deserializeErrors } from "./lib/dom/ssr/errors";
-
/** @internal */
export { RemixErrorBoundary as UNSAFE_RemixErrorBoundary } from "./lib/dom/ssr/errorBoundaries";
diff --git a/packages/react-router/lib/dom-export/hydrated-router.tsx b/packages/react-router/lib/dom-export/hydrated-router.tsx
index 9183feb344..3d4b8b9649 100644
--- a/packages/react-router/lib/dom-export/hydrated-router.tsx
+++ b/packages/react-router/lib/dom-export/hydrated-router.tsx
@@ -17,7 +17,6 @@ import {
UNSAFE_createBrowserHistory as createBrowserHistory,
UNSAFE_createClientRoutes as createClientRoutes,
UNSAFE_createRouter as createRouter,
- UNSAFE_deserializeErrors as deserializeErrors,
UNSAFE_getTurboStreamSingleFetchDataStrategy as getTurboStreamSingleFetchDataStrategy,
UNSAFE_getPatchRoutesOnNavigationFunction as getPatchRoutesOnNavigationFunction,
UNSAFE_useFogOFWarDiscovery as useFogOFWarDiscovery,
@@ -159,11 +158,6 @@ function createHydratedRouter({
isSpaMode: ssrInfo.context.isSpaMode,
});
- if (hydrationData && hydrationData.errors) {
- // TODO: De-dup this or remove entirely in v7 where single fetch is the
- // only approach and we have already serialized or deserialized on the server
- hydrationData.errors = deserializeErrors(hydrationData.errors);
- }
}
// We cannot support history-state-driven masking with SSR, so if a hard
diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx
index 298c809824..58937241b7 100644
--- a/packages/react-router/lib/dom/lib.tsx
+++ b/packages/react-router/lib/dom/lib.tsx
@@ -36,6 +36,7 @@ import type {
} from "../router/utils";
import {
ErrorResponseImpl,
+ SUPPORTED_ERROR_TYPES,
joinPaths,
matchPath,
parseToInfo,
@@ -740,7 +741,10 @@ function deserializeErrors(
);
} else if (val && val.__type === "Error") {
// Attempt to reconstruct the right type of Error (i.e., ReferenceError)
- if (val.__subType) {
+ if (
+ typeof val.__subType === "string" &&
+ SUPPORTED_ERROR_TYPES.includes(val.__subType)
+ ) {
let ErrorConstructor = window[val.__subType];
if (typeof ErrorConstructor === "function") {
try {
diff --git a/packages/react-router/lib/dom/ssr/errors.ts b/packages/react-router/lib/dom/ssr/errors.ts
deleted file mode 100644
index 3bdd82c414..0000000000
--- a/packages/react-router/lib/dom/ssr/errors.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import type { RouterState } from "../../router/router";
-import { ErrorResponseImpl } from "../../router/utils";
-
-export function deserializeErrors(
- errors: RouterState["errors"],
-): RouterState["errors"] {
- if (!errors) return null;
- let entries = Object.entries(errors);
- let serialized: RouterState["errors"] = {};
- for (let [key, val] of entries) {
- // Hey you! If you change this, please change the corresponding logic in
- // serializeErrors in react-router/lib/server-runtime/errors.ts :)
- if (val && val.__type === "RouteErrorResponse") {
- serialized[key] = new ErrorResponseImpl(
- val.status,
- val.statusText,
- val.data,
- val.internal === true,
- );
- } else if (val && val.__type === "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);
- error.stack = val.stack;
- serialized[key] = error;
- } catch (
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- e
- ) {
- // no-op - fall through and create a normal Error
- }
- }
- }
-
- if (serialized[key] == null) {
- let error = new Error(val.message);
- error.stack = val.stack;
- serialized[key] = error;
- }
- } else {
- serialized[key] = val;
- }
- }
- return serialized;
-}
diff --git a/packages/react-router/lib/dom/ssr/single-fetch.tsx b/packages/react-router/lib/dom/ssr/single-fetch.tsx
index 2860c51267..42fd58b0e1 100644
--- a/packages/react-router/lib/dom/ssr/single-fetch.tsx
+++ b/packages/react-router/lib/dom/ssr/single-fetch.tsx
@@ -1,9 +1,6 @@
import * as React from "react";
-import {
- SUPPORTED_ERROR_TYPES,
- decode,
-} from "../../../vendor/turbo-stream-v2/turbo-stream";
+import { decode } from "../../../vendor/turbo-stream-v2/turbo-stream";
import type { Router as DataRouter } from "../../router/router";
import { isDataWithResponseInit, isResponse } from "../../router/router";
import type {
@@ -14,6 +11,7 @@ import type {
} from "../../router/utils";
import {
ErrorResponseImpl,
+ SUPPORTED_ERROR_TYPES,
isRouteErrorResponse,
redirect,
data,
diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts
index 0596fffc45..f1eeb0c292 100644
--- a/packages/react-router/lib/router/utils.ts
+++ b/packages/react-router/lib/router/utils.ts
@@ -2109,6 +2109,15 @@ export type ErrorResponse = {
data: any;
};
+export const SUPPORTED_ERROR_TYPES = [
+ "EvalError",
+ "RangeError",
+ "ReferenceError",
+ "SyntaxError",
+ "TypeError",
+ "URIError",
+];
+
/*
* Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies
*
diff --git a/packages/react-router/lib/server-runtime/server.ts b/packages/react-router/lib/server-runtime/server.ts
index dcc15f5869..8cb9d16c5a 100644
--- a/packages/react-router/lib/server-runtime/server.ts
+++ b/packages/react-router/lib/server-runtime/server.ts
@@ -21,7 +21,7 @@ import type { AppLoadContext } from "./data";
import type { HandleErrorFunction, ServerBuild } from "./build";
import type { CriticalCss, EntryContext } from "../dom/ssr/entry";
import { createEntryRouteModules } from "./entry";
-import { sanitizeErrors, serializeError, serializeErrors } from "./errors";
+import { sanitizeErrors, serializeError } from "./errors";
import { ServerMode, isServerMode } from "./mode";
import type { RouteMatch } from "./routeMatching";
import { matchServerRoutes } from "./routeMatching";
@@ -550,7 +550,7 @@ async function handleDocumentRequest(
let state = {
loaderData: context.loaderData,
actionData: context.actionData,
- errors: serializeErrors(context.errors, serverMode),
+ errors: context.errors,
};
let baseServerHandoff: ServerHandoff = {
basename: build.basename,
@@ -636,7 +636,7 @@ async function handleDocumentRequest(
let state = {
loaderData: context.loaderData,
actionData: context.actionData,
- errors: serializeErrors(context.errors, serverMode),
+ errors: context.errors,
};
entryContext = {
...entryContext,
diff --git a/packages/react-router/vendor/turbo-stream-v2/turbo-stream.ts b/packages/react-router/vendor/turbo-stream-v2/turbo-stream.ts
index 3128162d86..791a75836d 100644
--- a/packages/react-router/vendor/turbo-stream-v2/turbo-stream.ts
+++ b/packages/react-router/vendor/turbo-stream-v2/turbo-stream.ts
@@ -1,8 +1,8 @@
+import { SUPPORTED_ERROR_TYPES } from "../../lib/router/utils";
import { flatten } from "./flatten";
import { unflatten } from "./unflatten";
import {
Deferred,
- SUPPORTED_ERROR_TYPES,
TYPE_ERROR,
TYPE_PREVIOUS_RESOLVED,
TYPE_PROMISE,
diff --git a/packages/react-router/vendor/turbo-stream-v2/unflatten.ts b/packages/react-router/vendor/turbo-stream-v2/unflatten.ts
index 86e45358ad..0c146a930f 100644
--- a/packages/react-router/vendor/turbo-stream-v2/unflatten.ts
+++ b/packages/react-router/vendor/turbo-stream-v2/unflatten.ts
@@ -1,3 +1,4 @@
+import { SUPPORTED_ERROR_TYPES } from "../../lib/router/utils";
import {
Deferred,
HOLE,
@@ -19,7 +20,6 @@ import {
TYPE_SYMBOL,
TYPE_URL,
type ThisDecode,
- SUPPORTED_ERROR_TYPES,
} from "./utils";
const globalObj = (
diff --git a/packages/react-router/vendor/turbo-stream-v2/utils.ts b/packages/react-router/vendor/turbo-stream-v2/utils.ts
index 51ccc7765f..e091c55318 100644
--- a/packages/react-router/vendor/turbo-stream-v2/utils.ts
+++ b/packages/react-router/vendor/turbo-stream-v2/utils.ts
@@ -44,15 +44,6 @@ export interface ThisEncode {
signal?: AbortSignal;
}
-export const SUPPORTED_ERROR_TYPES = [
- "EvalError",
- "RangeError",
- "ReferenceError",
- "SyntaxError",
- "TypeError",
- "URIError",
-];
-
export class Deferred