diff --git a/src/lib/__tests__/api-handler-safe-request-prop.test.ts b/src/lib/__tests__/api-handler-safe-request-prop.test.ts index defbf99c8..b15f75192 100644 --- a/src/lib/__tests__/api-handler-safe-request-prop.test.ts +++ b/src/lib/__tests__/api-handler-safe-request-prop.test.ts @@ -33,47 +33,69 @@ vi.mock("next/headers", () => ({ import { __testables } from "@/lib/api-handler"; -const { safeRequestProp, isPrivateFieldAccessError } = __testables; +const { safeRequestProp, isTolerableRequestProbeError } = __testables; -describe("isPrivateFieldAccessError — narrow-catch classifier", () => { +describe("isTolerableRequestProbeError — narrow-catch classifier", () => { it("returns true for the V8 private-field TypeError", () => { const err = new TypeError( "Cannot read private member #state from an object whose class did not declare it", ); - expect(isPrivateFieldAccessError(err)).toBe(true); + expect(isTolerableRequestProbeError(err)).toBe(true); }); it("returns true for the alternative 'private field' wording", () => { const err = new TypeError( "Cannot read private field #foo from object that was not created from its class", ); - expect(isPrivateFieldAccessError(err)).toBe(true); + expect(isTolerableRequestProbeError(err)).toBe(true); }); it("returns true for the Bun / older-V8 'private name' wording", () => { const err = new TypeError("Cannot read private name #bar from object"); - expect(isPrivateFieldAccessError(err)).toBe(true); + expect(isTolerableRequestProbeError(err)).toBe(true); + }); + + it("returns true when the request handed in is undefined or null", () => { + expect( + isTolerableRequestProbeError( + new TypeError("Cannot read properties of undefined (reading 'url')"), + ), + ).toBe(true); + expect( + isTolerableRequestProbeError( + new TypeError("Cannot read properties of null (reading 'method')"), + ), + ).toBe(true); + expect( + isTolerableRequestProbeError( + new TypeError("Cannot read property 'headers' of undefined"), + ), + ).toBe(true); }); it("returns false for any other TypeError (real bug, must surface)", () => { expect( - isPrivateFieldAccessError(new TypeError("undefined is not a function")), + isTolerableRequestProbeError( + new TypeError("undefined is not a function"), + ), ).toBe(false); expect( - isPrivateFieldAccessError(new TypeError("Cannot read properties of null")), + isTolerableRequestProbeError( + new TypeError("Cannot read properties of {} (reading 'parse')"), + ), ).toBe(false); }); it("returns false for non-TypeError exceptions", () => { - expect(isPrivateFieldAccessError(new Error("private member #x"))).toBe( + expect(isTolerableRequestProbeError(new Error("private member #x"))).toBe( false, ); - expect(isPrivateFieldAccessError(new RangeError("private member"))).toBe( - false, - ); - expect(isPrivateFieldAccessError("private member string")).toBe(false); - expect(isPrivateFieldAccessError(null)).toBe(false); - expect(isPrivateFieldAccessError(undefined)).toBe(false); + expect( + isTolerableRequestProbeError(new RangeError("private member")), + ).toBe(false); + expect(isTolerableRequestProbeError("private member string")).toBe(false); + expect(isTolerableRequestProbeError(null)).toBe(false); + expect(isTolerableRequestProbeError(undefined)).toBe(false); }); }); diff --git a/src/lib/api-handler.ts b/src/lib/api-handler.ts index 3683c3c61..cf36d5271 100644 --- a/src/lib/api-handler.ts +++ b/src/lib/api-handler.ts @@ -41,18 +41,32 @@ export class HttpError extends Error { // and fall back to safe defaults so logging instrumentation never // crashes the handler. // -// The catch is narrowed to the V8 private-field TypeError. Any other +// The catch is narrowed to two well-known shapes: the V8 private-field +// TypeError, and the `Cannot read properties of undefined/null` shape +// raised when the wrapper is invoked without a request (vitest tests +// frequently invoke handlers as `GET()` with no args, mirroring the +// shape Next.js exercises for the static-export pass). Any other // exception is a real bug in the read callback or in a downstream -// header parser and must surface — swallowing it here would hide +// header parser and must surface — swallowing it would hide // regressions in production instrumentation. -function isPrivateFieldAccessError(err: unknown): boolean { +function isTolerableRequestProbeError(err: unknown): boolean { if (!(err instanceof TypeError)) return false; const msg = err.message ?? ""; return ( + // V8 — current msg.includes("private member") || + // V8 — alternative msg.includes("private field") || - // Bun / older V8 wording variant - msg.includes("private name") + // Bun / older V8 + msg.includes("private name") || + // No request handed in at all (vitest direct-invoke or + // force-static placeholder reduced to undefined / null). + // Covers both modern `Cannot read properties of undefined + // (reading 'X')` and the older `Cannot read property 'X' of + // undefined` wordings. + /Cannot read propert(?:y|ies)\b.*\bof (?:undefined|null)\b/.test( + msg, + ) ); } @@ -64,7 +78,7 @@ function safeRequestProp( try { return read(request as NextRequest); } catch (err) { - if (isPrivateFieldAccessError(err)) return fallback; + if (isTolerableRequestProbeError(err)) return fallback; throw err; } } @@ -72,7 +86,7 @@ function safeRequestProp( /** @internal — exposed for unit tests of the narrow-catch contract. */ export const __testables = { safeRequestProp, - isPrivateFieldAccessError, + isTolerableRequestProbeError, }; // Next.js route handlers come in two shapes — `(request)` for static routes