Skip to content

Commit

Permalink
Support proper hydration of Error subclasses (#6675)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Jun 30, 2023
1 parent 0657c16 commit 12af64b
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 4 deletions.
6 changes: 6 additions & 0 deletions .changeset/hydrate-error-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@remix-run/react": patch
"@remix-run/server-runtime": patch
---

Support proper hydration of `Error` subclasses such as `ReferenceError`/`TypeError` in development mode
44 changes: 43 additions & 1 deletion integration/error-sanitization-test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { test, expect } from "@playwright/test";
import { ServerMode } from "@remix-run/server-runtime/mode";

import { createFixture, js } from "./helpers/create-fixture";
import type { Fixture } from "./helpers/create-fixture";
import { createAppFixture, createFixture, js } from "./helpers/create-fixture";
import { PlaywrightFixture } from "./helpers/playwright-fixture";

const routeFiles = {
"app/root.jsx": js`
Expand Down Expand Up @@ -33,6 +34,10 @@ const routeFiles = {
if (new URL(request.url).searchParams.has('loader')) {
throw new Error("Loader Error");
}
if (new URL(request.url).searchParams.has('subclass')) {
// This will throw a ReferenceError
console.log(thisisnotathing);
}
return "LOADER"
}
Expand All @@ -58,6 +63,7 @@ const routeFiles = {
<>
<h1>Index Error</h1>
<p>{"MESSAGE:" + error.message}</p>
<p>{"NAME:" + error.name}</p>
{error.stack ? <p>{"STACK:" + error.stack}</p> : null}
</>
);
Expand Down Expand Up @@ -279,6 +285,21 @@ test.describe("Error Sanitization", () => {
);
expect(errorLogs[0][0].stack).toMatch(" at ");
});

test("does not support hydration of Error subclasses", async ({ page }) => {
let response = await fixture.requestDocument("/?subclass");
let html = await response.text();
expect(html).toMatch("<p>MESSAGE:Unexpected Server Error");
expect(html).toMatch("<p>NAME:Error");

// Hydration
let appFixture = await createAppFixture(fixture);
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/?subclass", true);
html = await app.getHtml();
expect(html).toMatch("<p>MESSAGE:Unexpected Server Error");
expect(html).toMatch("<p>NAME:Error");
});
});

test.describe("serverMode=development", () => {
Expand Down Expand Up @@ -428,6 +449,27 @@ test.describe("Error Sanitization", () => {
);
expect(errorLogs[0][0].stack).toMatch(" at ");
});

test("supports hydration of Error subclasses", async ({ page }) => {
let response = await fixture.requestDocument("/?subclass");
let html = await response.text();
expect(html).toMatch("<p>MESSAGE:thisisnotathing is not defined");
expect(html).toMatch("<p>NAME:ReferenceError");
expect(html).toMatch(
"<p>STACK:ReferenceError: thisisnotathing is not defined"
);

// Hydration
let appFixture = await createAppFixture(fixture, ServerMode.Development);
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/?subclass", true);
html = await app.getHtml();
expect(html).toMatch("<p>MESSAGE:thisisnotathing is not defined");
expect(html).toMatch("<p>NAME:ReferenceError");
expect(html).toMatch(
"STACK:ReferenceError: thisisnotathing is not defined"
);
});
});

test.describe("serverMode=production (user-provided handleError)", () => {
Expand Down
23 changes: 20 additions & 3 deletions packages/remix-react/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,26 @@ export function deserializeErrors(
val.internal === true
);
} else if (val && val.__type === "Error") {
let error = new Error(val.message);
error.stack = val.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);
error.stack = val.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);
error.stack = val.stack;
serialized[key] = error;
}
} else {
serialized[key] = val;
}
Expand Down
9 changes: 9 additions & 0 deletions packages/remix-server-runtime/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ export function serializeErrors(
message: sanitized.message,
stack: sanitized.stack,
__type: "Error",
// If this is a subclass (i.e., ReferenceError), send up the type so we
// can re-create the same type during hydration. This will only apply
// in dev mode since all production errors are sanitized to normal
// Error instances
...(sanitized.name !== "Error"
? {
__subType: sanitized.name,
}
: {}),
};
} else {
serialized[key] = val;
Expand Down

0 comments on commit 12af64b

Please sign in to comment.