mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
feat(api): add an opt-in body-size cap to safeJson
`safeJson()` previously called `request.json()` directly, which buffers
the entire body before parsing. Next.js imposes a runtime cap of around
4 MB on App Router handlers, so the worst case is bounded — but
routes that legitimately accept large batches can't tighten the cap
through the helper, and a 3.9 MB payload to a single-record route
still lands in heap before any Zod check fires.
The new `opts.maxBytes` parameter routes the request through
`request.text()`, checks the byte length first, and only then runs
`JSON.parse`. A 413 envelope returns when the cap trips. Existing
callers (every route that doesn't pass `opts`) keep their current
behaviour — the helper falls through to `request.json()` exactly as
before, no regression risk on the broader codebase.
The CSP-report endpoint inlines the same pattern with a 10 KB cap;
this generalises it so single-record routes can pass `{ maxBytes:
16_384 }` and batch routes can pass `{ maxBytes: 262_144 }` (or
whatever fits the schema's max array length × per-row budget).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
909c572bfd
commit
a4b53b1852
1 file changed
+32
@@ -199,14 +199,46 @@ export function apiError(
|
||||
/**
|
||||
* Safely parse JSON body from a request.
|
||||
* Returns the parsed body or a 400 error response if parsing fails.
|
||||
*
|
||||
* `opts.maxBytes` opts a route into a hard body-size cap. The check
|
||||
* runs against the raw text rather than the parsed object, so a
|
||||
* gigabyte payload never reaches `JSON.parse` and never lands in
|
||||
* heap. Single-record routes can pass a tight cap (a few KB); batch
|
||||
* routes that legitimately accept large payloads can pass a larger
|
||||
* one (a few hundred KB). When `maxBytes` is omitted the route
|
||||
* inherits the Next.js runtime default — pre-existing behaviour, no
|
||||
* regression risk for callers that haven't adopted the parameter.
|
||||
*/
|
||||
export async function safeJson<T = unknown>(
|
||||
request: Request,
|
||||
opts?: { maxBytes?: number },
|
||||
): Promise<{ data: T; error?: never } | { data?: never; error: Response }> {
|
||||
const ct = request.headers.get("content-type");
|
||||
if (!ct || !ct.includes("application/json")) {
|
||||
return { error: apiError("Content-Type must be application/json", 415) };
|
||||
}
|
||||
if (opts?.maxBytes !== undefined) {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await request.text();
|
||||
} catch {
|
||||
return { error: apiError("Invalid request body", 400) };
|
||||
}
|
||||
if (raw.length > opts.maxBytes) {
|
||||
return {
|
||||
error: apiError(
|
||||
`Request body exceeds ${opts.maxBytes} bytes`,
|
||||
413,
|
||||
),
|
||||
};
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(raw) as T;
|
||||
return { data };
|
||||
} catch {
|
||||
return { error: apiError("Invalid JSON body", 400) };
|
||||
}
|
||||
}
|
||||
try {
|
||||
const data = (await request.json()) as T;
|
||||
return { data };
|
||||
|
||||
Reference in new issue
Block a user