mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
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:
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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user