fix(api-handler): widen safeRequestProp narrow-catch to tolerate undefined requests

Follow-up to commit `61d156e7`. The narrow catch initially recognised
only the V8 private-field TypeError wordings, but unit tests routinely
invoke route handlers as `GET()` with no arguments — the wrapper then
reads `args[0]` as `undefined` and the read callback raises
`TypeError: Cannot read properties of undefined (reading 'url')`,
which is not a private-field error.

The classifier is renamed to `isTolerableRequestProbeError` and now
also matches the "cannot read properties of undefined / null"
TypeError shape (both modern V8 wording and the older
`Cannot read property 'X' of undefined` form). Any other TypeError
still rethrows so production regressions stay visible.

This restores the no-arg-direct-invoke contract that
`src/app/api/auth/me/source-priority/__tests__/route.test.ts` and
similar vitest suites rely on (`GET()` with no request to assert
the auth-gate path returns 401).

Verification:
  pnpm typecheck                                       → clean
  pnpm vitest run api-handler-safe-request-prop        → 11 / 11 pass
  pnpm vitest run src/app/api/auth/me/source-priority  → 6 / 6 pass
  pnpm vitest run (full unit)                          → 3751 / 3751
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 21:50:29 +02:00
1 parent bbc0afd555
commit fcda115123
2 files changed
+57 -21

No files matched your search

@@ -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);
});
});
+21 -7
View File
@@ -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<R>(
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<R>(
/** @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