Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions src/lib/crash-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,27 @@ export function isBenignAbortTeardown(err: unknown): boolean {
const lockedStreamTeardown = err.message === "Invalid state: ReadableStream is locked"
&& (err as { code?: unknown }).code === "ERR_INVALID_STATE";
if (!bareNullTeardown && !lockedStreamTeardown) return false;

const details = err as TypeError & {
sourceURL?: unknown;
line?: unknown;
column?: unknown;
originalLine?: unknown;
originalColumn?: unknown;
};
if (
(typeof details.sourceURL === "string" && details.sourceURL.length > 0)
|| details.line !== undefined
|| details.column !== undefined
|| details.originalLine !== undefined
|| details.originalColumn !== undefined
) return false;

const stack = err.stack ?? "";
// Native-only: no JS source frame. A real app TypeError would carry a `(file:line:col)` frame.
return !/\((?!native:)[^)]*:\d+:\d+\)/.test(stack);
// Native-only: no JS source frame, whether parenthesized or not.
return !stack.split(/\r?\n/).slice(1).some(line =>
/:\d+:\d+\)?\s*$/.test(line) && !/\bnative:\d+:\d+\)?\s*$/.test(line),
);
}

function record(kind: string, err: unknown, promise?: unknown): void {
Expand Down
23 changes: 22 additions & 1 deletion tests/crash-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ describe("crash-guard diagnostics", () => {
describe("benign abort-teardown classification", () => {
test("flags the native-only bare TypeError as benign", () => {
const err = new TypeError("null is not an object");
Object.assign(err, { sourceURL: "", line: undefined, column: undefined, originalLine: undefined, originalColumn: undefined });
err.stack = "TypeError: null is not an object\n at <anonymous> (native:1:11)\n at processTicksAndRejections (native:7:39)";
expect(isBenignAbortTeardown(err)).toBe(true);
});
Expand All @@ -118,6 +119,19 @@ describe("benign abort-teardown classification", () => {
expect(isBenignAbortTeardown(err)).toBe(false);
});

test("does NOT flag a TypeError with an unparenthesized JS source frame", () => {
const err = new TypeError("null is not an object");
err.stack = "TypeError: null is not an object\n at /abs/src/server.ts:120:13";
expect(isBenignAbortTeardown(err)).toBe(false);
});

test("does NOT flag a TypeError with hidden JSC source fields", () => {
const err = new TypeError("null is not an object");
err.stack = "TypeError: null is not an object\n at <anonymous> (native:1:11)";
Object.assign(err, { sourceURL: "/abs/src/server.ts", originalLine: 120, originalColumn: 13 });
expect(isBenignAbortTeardown(err)).toBe(false);
});

test("does NOT flag a different message or the (evaluating …) form", () => {
const a = new TypeError("null is not an object (evaluating 'x.y')");
a.stack = "TypeError: ...\n at <anonymous> (native:1:11)";
Expand All @@ -136,7 +150,14 @@ describe("benign abort-teardown classification", () => {

test("flags the native-only locked-ReadableStream sink-close teardown as benign (260712)", () => {
const err = new TypeError("Invalid state: ReadableStream is locked");
(err as { code?: string }).code = "ERR_INVALID_STATE";
Object.assign(err, {
code: "ERR_INVALID_STATE",
sourceURL: "",
line: undefined,
column: undefined,
originalLine: undefined,
originalColumn: undefined,
});
err.stack = "TypeError: Invalid state: ReadableStream is locked\n at unknown\n at <anonymous> (native:1:11)\n at onSinkClose2 (native:5:32)";
expect(isBenignAbortTeardown(err)).toBe(true);
});
Expand Down
Loading