Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🗺 Fix server error logging and add onUnhandledError support #6495

Merged
merged 12 commits into from
Jun 1, 2023
5 changes: 5 additions & 0 deletions .changeset/log-server-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/server-runtime": patch
---

Ensure un-sanitized server errors are logged on the server during document requests
50 changes: 49 additions & 1 deletion integration/error-sanitization-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,12 @@ const routeFiles = {
test.describe("Error Sanitization", () => {
let fixture: Fixture;
let oldConsoleError: () => void;
let errorLogs: any[] = [];

test.beforeEach(() => {
oldConsoleError = console.error;
console.error = () => {};
errorLogs = [];
console.error = (...args) => errorLogs.push(args);
});

test.afterEach(() => {
Expand Down Expand Up @@ -167,6 +169,9 @@ test.describe("Error Sanitization", () => {
'{"routes/index":{"message":"Unexpected Server Error","__type":"Error"}}'
);
expect(html).not.toMatch(/stack/i);
expect(errorLogs.length).toBe(1);
expect(errorLogs[0][0].message).toMatch("Loader Error");
expect(errorLogs[0][0].stack).toMatch(" at ");
});

test("sanitizes render errors in document requests", async () => {
Expand All @@ -178,6 +183,9 @@ test.describe("Error Sanitization", () => {
'{"routes/index":{"message":"Unexpected Server Error","__type":"Error"}}'
);
expect(html).not.toMatch(/stack/i);
expect(errorLogs.length).toBe(1);
expect(errorLogs[0][0].message).toMatch("Render Error");
expect(errorLogs[0][0].stack).toMatch(" at ");
});

test("renders deferred document without errors", async () => {
Expand All @@ -200,6 +208,9 @@ test.describe("Error Sanitization", () => {
// Defer errors are not not part of the JSON blob but rather rejected
// against a pending promise and therefore are inlined JS.
expect(html).toMatch("x.stack=undefined;");
// defer errors are not logged to the server console since the request
// has "succeeded"
expect(errorLogs.length).toBe(0);
});

test("returns data without errors", async () => {
Expand All @@ -214,6 +225,9 @@ test.describe("Error Sanitization", () => {
let response = await fixture.requestData("/?loader", "routes/index");
let text = await response.text();
expect(text).toBe('{"message":"Unexpected Server Error"}');
expect(errorLogs.length).toBe(1);
expect(errorLogs[0][0].message).toMatch("Loader Error");
expect(errorLogs[0][0].stack).toMatch(" at ");
});

test("returns deferred data without errors", async () => {
Expand All @@ -231,6 +245,9 @@ test.describe("Error Sanitization", () => {
'{"lazy":"__deferred_promise:lazy"}\n\n' +
'error:{"lazy":{"message":"Unexpected Server Error"}}\n\n'
);
// defer errors are not logged to the server console since the request
// has "succeeded"
expect(errorLogs.length).toBe(0);
});

test("sanitizes loader errors in resource requests", async () => {
Expand All @@ -240,12 +257,20 @@ test.describe("Error Sanitization", () => {
);
let text = await response.text();
expect(text).toBe('{"message":"Unexpected Server Error"}');
expect(errorLogs.length).toBe(1);
expect(errorLogs[0][0].message).toMatch("Loader Error");
expect(errorLogs[0][0].stack).toMatch(" at ");
});

test("sanitizes mismatched route errors in data requests", async () => {
let response = await fixture.requestData("/", "not-a-route");
let text = await response.text();
expect(text).toBe('{"message":"Unexpected Server Error"}');
expect(errorLogs.length).toBe(1);
expect(errorLogs[0][0].message).toMatch(
'Route "not-a-route" does not match URL "/"'
);
expect(errorLogs[0][0].stack).toMatch(" at ");
});
});

Expand Down Expand Up @@ -286,6 +311,9 @@ test.describe("Error Sanitization", () => {
expect(html).toMatch(
'errors":{"routes/index":{"message":"Loader Error","stack":"Error: Loader Error\\n'
);
expect(errorLogs.length).toBe(1);
expect(errorLogs[0][0].message).toMatch("Loader Error");
expect(errorLogs[0][0].stack).toMatch(" at ");
});

test("does not sanitize render errors in document requests", async () => {
Expand All @@ -297,6 +325,9 @@ test.describe("Error Sanitization", () => {
expect(html).toMatch(
'errors":{"routes/index":{"message":"Render Error","stack":"Error: Render Error\\n'
);
expect(errorLogs.length).toBe(1);
expect(errorLogs[0][0].message).toMatch("Render Error");
expect(errorLogs[0][0].stack).toMatch(" at ");
});

test("renders deferred document without errors", async () => {
Expand All @@ -316,6 +347,9 @@ test.describe("Error Sanitization", () => {
// Defer errors are not not part of the JSON blob but rather rejected
// against a pending promise and therefore are inlined JS.
expect(html).toMatch("x.stack=e.stack;");
// defer errors are not logged to the server console since the request
// has "succeeded"
expect(errorLogs.length).toBe(0);
});

test("returns data without errors", async () => {
Expand All @@ -332,6 +366,9 @@ test.describe("Error Sanitization", () => {
expect(text).toMatch(
'{"message":"Loader Error","stack":"Error: Loader Error'
);
expect(errorLogs.length).toBe(1);
expect(errorLogs[0][0].message).toMatch("Loader Error");
expect(errorLogs[0][0].stack).toMatch(" at ");
});

test("returns deferred data without errors", async () => {
Expand All @@ -348,6 +385,9 @@ test.describe("Error Sanitization", () => {
expect(text).toMatch(
'error:{"lazy":{"message":"REJECTED","stack":"Error: REJECTED'
);
// defer errors are not logged to the server console since the request
// has "succeeded"
expect(errorLogs.length).toBe(0);
});

test("does not sanitize loader errors in resource requests", async () => {
Expand All @@ -359,6 +399,9 @@ test.describe("Error Sanitization", () => {
expect(text).toMatch(
'{"message":"Loader Error","stack":"Error: Loader Error'
);
expect(errorLogs.length).toBe(1);
expect(errorLogs[0][0].message).toMatch("Loader Error");
expect(errorLogs[0][0].stack).toMatch(" at ");
});

test("sanitizes mismatched route errors in data requests", async () => {
Expand All @@ -367,6 +410,11 @@ test.describe("Error Sanitization", () => {
expect(text).toMatch(
'{"message":"Route \\"not-a-route\\" does not match URL \\"/\\"","stack":"Error: Route \\"not-a-route\\" does not match URL \\"/\\"'
);
expect(errorLogs.length).toBe(1);
expect(errorLogs[0][0].message).toMatch(
'Route "not-a-route" does not match URL "/"'
);
expect(errorLogs[0][0].stack).toMatch(" at ");
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export default async function handleRequest(
{
signal: request.signal,
onError(error: unknown) {
console.error(error);
responseStatusCode = 500;
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export default async function handleRequest(
{
signal: request.signal,
onError(error: unknown) {
console.error(error);
responseStatusCode = 500;
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ function handleBotRequest(
},
onError(error: unknown) {
responseStatusCode = 500;
console.error(error);
},
}
);
Expand Down Expand Up @@ -103,7 +102,6 @@ function handleBrowserRequest(
reject(error);
},
onError(error: unknown) {
console.error(error);
responseStatusCode = 500;
},
}
Expand Down
1 change: 0 additions & 1 deletion packages/remix-react/errorBoundaries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ export class RemixErrorBoundary extends React.Component<
* When app's don't provide a root level ErrorBoundary, we default to this.
*/
export function RemixRootDefaultErrorBoundary({ error }: { error: Error }) {
console.error(error);
brophdawg11 marked this conversation as resolved.
Show resolved Hide resolved
return (
<html lang="en">
<head>
Expand Down
5 changes: 5 additions & 0 deletions packages/remix-server-runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,9 @@ async function handleDocumentRequestRR(

// Sanitize errors outside of development environments
if (context.errors) {
Object.values(context.errors).forEach((err) =>
logServerErrorIfNotAborted(err, request, serverMode)
);
context.errors = sanitizeErrors(context.errors, serverMode);
}

Expand Down Expand Up @@ -284,6 +287,8 @@ async function handleDocumentRequestRR(
loadContext
);
} catch (error: unknown) {
logServerErrorIfNotAborted(error, request, serverMode);

// Get a new StaticHandlerContext that contains the error at the right boundary
context = getStaticContextFromError(
staticHandler.dataRoutes,
Expand Down