-
Notifications
You must be signed in to change notification settings - Fork 0
Body reading
The adapter reads JSON bodies from Fetch Request objects with a two-stage size guard: a Content-Length fast-path reject, then a mid-stream byte counter for requests without a declared length (chunked transfer) or whose CL lies.
Is Content-Length ≤ maxBodyBytes (or absent)?
├─ No → reject 413 PayloadTooLarge BEFORE reading any bytes
└─ Yes ─┐
▼
read request.body.getReader() chunk by chunk
│
▼
running byte total ≤ maxBodyBytes?
├─ No → cancel the reader, reject 413
└─ Yes → decode UTF-8, accumulate
│
▼
empty? → return null
non-empty?
├─ JSON.parse OK → return value
└─ JSON.parse throws → reject 400 BadJsonBody
request.json() is a one-liner but has no size cap; a Content-Length: 1GB header plus 1 GB of data will OOM the runtime before throwing. Pure streaming with getReader() is safe but costs allocations for the well-behaved case. The hybrid gives:
- Zero-byte reject for uploads that honestly declare their size above the cap.
- Safe mid-stream reject for chunked / CL-liar requests — a running counter catches them before the streaming decoder grows out of bounds.
Under attack load (CL = 2 GB, maxBodyBytes = 1 MiB), the fast-path costs one header read; the streaming fallback would cost at least one chunk-read.
const handler = createFetchAdapter(planets); // 1 MiB by defaultmaxBodyBytes defaults to 1048576 — matches the bundled node:http handler plus the koa / express adapters. Override per adapter:
// Low-power device — cap at 64 KiB
createFetchAdapter(planets, {maxBodyBytes: 64 * 1024});
// Dedicated bulk-load — allow larger bodies
createFetchAdapter(bulk, {maxBodyBytes: 16 * 1024 * 1024});| Condition | Status | code |
|---|---|---|
| Content-Length over cap | 413 |
PayloadTooLarge |
| Body streamed past cap | 413 |
PayloadTooLarge |
| Body is not valid JSON | 400 |
BadJsonBody |
PUT /-load body is not an array |
400 |
BadLoadBody |
Body is null for requests with no payload (empty body on POST, or GET-shaped Requests).
src/read-web-body.js is exported as a sub-export if you need cap-enforced JSON reading in other handlers:
import {readJsonBody} from 'dynamodb-toolkit-fetch/read-web-body.js';
const body = await readJsonBody(request, 1024 * 1024);
// body: parsed JSON, or null for empty body
// throws: {status:413,code:'PayloadTooLarge'} or {status:400,code:'BadJsonBody'}Normally you won't need this — the adapter handles it internally.
When you mount multiple adapters and want different caps per route prefix, pass maxBodyBytes per instance. Each adapter holds its own cap; they don't share state:
const planets = createFetchAdapter(planetsAdapter, {mountPath: '/planets', maxBodyBytes: 256 * 1024});
const bulk = createFetchAdapter(bulkAdapter, {mountPath: '/bulk', maxBodyBytes: 16 * 1024 * 1024});See Composition for how to route between them on a single runtime.
reader.cancel() can reject if the stream source has already errored. The 413 we throw is authoritative — we wrap cancel() in try/catch, swallow its error, and let the 413 propagate. The cancel is just a hint to the runtime that the upstream can stop sending; backpressure and GC handle the dangling bytes.
Unlike the express adapter, there's no body-parser middleware to defer to. Fetch-handler hosts (Cloudflare Workers, Bun.serve, Deno.serve, Hono, itty-router) all pass the Request through with an unread body stream. The adapter always reads the body itself via readJsonBody.
If your host router has already consumed request.body (e.g. Hono's c.req.json() before delegating), pass the original raw Request — in Hono that's c.req.raw, itty-router exposes it similarly. See Composition for examples.
GET / and DELETE / don't read the body. DELETE /-by-names reads the body only when ?names= is absent. All other body-capable routes always read. The adapter's dispatch logic decides whether to call readJsonBody based on the route; unused body bytes stay in the runtime's buffer until GC.