From 3c773a1c1eb260f1a0fc7f37ab28173932152575 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Sun, 9 Aug 2026 15:38:34 +0900 Subject: [PATCH] fix(crash-guard): preserve application fault visibility --- src/lib/crash-guard.ts | 22 ++++++++++++++++++++-- tests/crash-guard.test.ts | 23 ++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/lib/crash-guard.ts b/src/lib/crash-guard.ts index e7661f5bc..6eddc939d 100644 --- a/src/lib/crash-guard.ts +++ b/src/lib/crash-guard.ts @@ -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 { diff --git a/tests/crash-guard.test.ts b/tests/crash-guard.test.ts index e879893e1..f2285ddce 100644 --- a/tests/crash-guard.test.ts +++ b/tests/crash-guard.test.ts @@ -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 (native:1:11)\n at processTicksAndRejections (native:7:39)"; expect(isBenignAbortTeardown(err)).toBe(true); }); @@ -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 (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 (native:1:11)"; @@ -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 (native:1:11)\n at onSinkClose2 (native:5:32)"; expect(isBenignAbortTeardown(err)).toBe(true); });