Skip to content

Commit d2cc324

Browse files
authored
fix: hide the error stack properly (#35)
1 parent e131e23 commit d2cc324

3 files changed

Lines changed: 133 additions & 4 deletions

File tree

src/error.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, expect, it } from "vitest";
2+
import { APIError } from "./error";
3+
import { inspect } from "node:util";
4+
5+
describe("APIError", () => {
6+
it("should throw correct stack", () => {
7+
const error = new APIError("INTERNAL_SERVER_ERROR", {
8+
message: "Test error",
9+
});
10+
expect(error.stack).toMatchInlineSnapshot(`"APIError: Test error"`);
11+
12+
function testError() {
13+
throw new APIError("INTERNAL_SERVER_ERROR", {
14+
message: "Test error in function",
15+
});
16+
}
17+
18+
function deepTestError() {
19+
testError();
20+
}
21+
22+
expect(() => deepTestError()).toThrowErrorMatchingInlineSnapshot(
23+
`[APIError: Test error in function]`,
24+
);
25+
26+
try {
27+
deepTestError();
28+
} catch (e: unknown) {
29+
const loggedString = inspect(e, {
30+
depth: Number.MAX_SAFE_INTEGER,
31+
});
32+
expect(loggedString).toMatchInlineSnapshot(`
33+
"[InternalAPIError: Test error in function] {
34+
status: 'INTERNAL_SERVER_ERROR',
35+
body: { code: 'TEST_ERROR_IN_FUNCTION', message: 'Test error in function' },
36+
headers: {},
37+
statusCode: 500
38+
}"
39+
`);
40+
const stack = (e as InstanceType<typeof APIError>).errorWithStack.stack;
41+
expect(stack).toMatch(
42+
new RegExp(
43+
"ErrorWithStack:\\s*\\n" +
44+
"\\s+at testError \\(.*/error\\.test\\.ts:\\d+:\\d+\\)\\n" +
45+
"\\s+at deepTestError \\(.*/error\\.test\\.ts:\\d+:\\d+\\)",
46+
"s",
47+
),
48+
);
49+
}
50+
});
51+
});

src/error.ts

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,72 @@
1+
// https://github.com/nodejs/node/blob/360f7cc7867b43344aac00564286b895e15f21d7/lib/internal/errors.js#L246C1-L261C2
2+
function isErrorStackTraceLimitWritable() {
3+
const desc = Object.getOwnPropertyDescriptor(Error, "stackTraceLimit");
4+
if (desc === undefined) {
5+
return Object.isExtensible(Error);
6+
}
7+
8+
return Object.prototype.hasOwnProperty.call(desc, "writable")
9+
? desc.writable
10+
: desc.set !== undefined;
11+
}
12+
13+
class ErrorWithStack extends Error {
14+
constructor() {
15+
super();
16+
this.name = "ErrorWithStack";
17+
}
18+
}
19+
20+
/**
21+
* Hide internal stack frames from the error stack trace.
22+
*/
23+
function hideInternalStackFrames(stack: string): string {
24+
const lines = stack.split("\n at ");
25+
if (lines.length <= 1) {
26+
return stack;
27+
}
28+
lines.splice(1, 1);
29+
return lines.join("\n at ");
30+
}
31+
32+
// https://github.com/nodejs/node/blob/360f7cc7867b43344aac00564286b895e15f21d7/lib/internal/errors.js#L411-L432
33+
function makeErrorForHideStackFrame<B extends new (...args: any[]) => Error>(
34+
Base: B,
35+
clazz: any,
36+
): {
37+
new (...args: ConstructorParameters<B>): InstanceType<B> & { errorWithStack: ErrorWithStack };
38+
} {
39+
class HideStackFramesError extends Base {
40+
#errorWithStack: ErrorWithStack;
41+
42+
constructor(...args: any[]) {
43+
if (isErrorStackTraceLimitWritable()) {
44+
const limit = Error.stackTraceLimit;
45+
Error.stackTraceLimit = 0;
46+
super(...args);
47+
Error.stackTraceLimit = limit;
48+
} else {
49+
super(...args);
50+
}
51+
this.#errorWithStack = new ErrorWithStack();
52+
this.#errorWithStack.stack = hideInternalStackFrames(this.#errorWithStack.stack ?? "");
53+
}
54+
55+
// use `getter` here to avoid the stack trace being captured by loggers
56+
get errorWithStack() {
57+
return this.#errorWithStack;
58+
}
59+
60+
// This is a workaround for wpt tests that expect that the error
61+
// constructor has a `name` property of the base class.
62+
get ["constructor"]() {
63+
return clazz;
64+
}
65+
}
66+
67+
return HideStackFramesError as any;
68+
}
69+
170
export const _statusCode = {
271
OK: 200,
372
CREATED: 201,
@@ -116,19 +185,27 @@ export type Status =
116185
| 510
117186
| 511;
118187

119-
export class APIError extends Error {
188+
class InternalAPIError extends Error {
120189
constructor(
121190
public status: keyof typeof _statusCode | Status = "INTERNAL_SERVER_ERROR",
122191
public body:
123192
| ({
124193
message?: string;
125194
code?: string;
195+
cause?: unknown;
126196
} & Record<string, any>)
127197
| undefined = undefined,
128198
public headers: HeadersInit = {},
129199
public statusCode = typeof status === "number" ? status : _statusCode[status],
130200
) {
131-
super(body?.message);
201+
super(
202+
body?.message,
203+
body?.cause
204+
? {
205+
cause: body.cause,
206+
}
207+
: undefined,
208+
);
132209
this.name = "APIError";
133210
this.status = status;
134211
this.headers = headers;
@@ -142,6 +219,8 @@ export class APIError extends Error {
142219
...body,
143220
}
144221
: undefined;
145-
this.stack = "";
146222
}
147223
}
224+
225+
export type APIError = InstanceType<typeof InternalAPIError>;
226+
export const APIError = makeErrorForHideStackFrame(InternalAPIError, Error);

src/router.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { createEndpoint, type Endpoint } from "./endpoint";
33
import { generator, getHTML } from "./openapi";
44
import type { Middleware } from "./middleware";
55
import { getBody, isAPIError } from "./utils";
6-
import { APIError } from "./error";
76
import { toResponse } from "./to-response";
87

98
export interface RouterConfig {

0 commit comments

Comments
 (0)