Limits and untrusted bodies
Limits are enforced before application code runs, in this order: URL length, method, host, body framing, then the body while it is read.
Limits are enforced before application code runs, in this order: URL length, method, host, body framing, then the body while it is read.
security.limits |
Default | Exceeding it |
|---|---|---|
maxUrlLength |
8192 characters | 414 |
maxBodyBytes |
1 MiB | 413, before the first byte when Content-Length says so, otherwise as soon as the stream passes the limit |
requestTimeoutMs |
30 000 | 504; event.signal is aborted so your fetches and queries can stop |
streamTimeoutMs |
120 000 | a streamed page that has not finished by then is cut off with an SSR_TIMEOUT error |
Two more bounds belong to server functions and hydration: rpc.maxGetPayloadChars (4096, 414 above it),
rpc.maxResponseBytes (8 MiB) and maxPayloadBytes (2 MiB of router data per page; a match that would exceed it is
left out of the payload). A method outside GET, HEAD, POST, PUT, PATCH, DELETE and OPTIONS gets 405 with Allow.
Framing is checked against request smuggling: a Content-Length that is not one plain number (the Fetch API joins
duplicate headers with a comma, so 5, 5 fails), Content-Length together with Transfer-Encoding, and any
transfer coding other than a single chunked are 400. The Node server rejects a repeated Content-Length,
Transfer-Encoding or Host line on the raw header list as well, and a Host that is not a host name or address.
/** Never answers; the request timeout does. */
export const GET = (): Promise<never> => new Promise<never>(() => undefined);
Reading a body#
Read JSON with readJson(request) and validate it (server functions validate for
you). It answers 415 unless the content type is application/json or application/*+json, and 400 for invalid JSON.
parseJsonSafe(text) also rejects what prototype pollution relies on: a __proto__ key at any depth, a
constructor or prototype key that holds an object, and nesting deeper than JSON_LIMITS.maxDepth (64). The
check is iterative, so a deeply nested body cannot overflow the stack. formToObject turns FormData or
URLSearchParams into a null-prototype object and drops those keys. readTextLimited(request, maxBytes) reads at
most maxBytes, for an endpoint that accepts less than the handler's limit, and throws BodyTooLargeError (413).
import {
BodyTooLargeError,
formToObject,
isHttpError,
parseJsonSafe,
readTextLimited,
} from "@shadow-js/dusk/server";
function attempt(label: string, source: string): void {
try {
const value = parseJsonSafe(source) as { a?: number };
console.log(`${label} ${value.a}`);
} catch (error) {
console.log(isHttpError(error) ? `${error.status} ${error.message}` : String(error));
}
}
attempt("ok", '{"a":1}');
attempt("proto", '{"a":1,"__proto__":{"admin":true}}');
attempt("ctor", '{"constructor":{"prototype":{"admin":true}}}');
attempt("deep", `${"[".repeat(70)}${"]".repeat(70)}`);
try {
await readTextLimited(new Request("http://localhost/", { method: "POST", body: "0123456789" }), 5);
} catch (error) {
if (error instanceof BodyTooLargeError) console.log(`${error.status} limit ${error.limit}`);
}
console.log(JSON.stringify(formToObject(new URLSearchParams("a=1&a=2&__proto__=x"))));
ok 1
400 Invalid JSON
400 Invalid JSON
400 Invalid JSON
413 limit 5
{Large bodies#
security.limits.maxBodyBytes is one limit for the whole app. The body is streamed through it, but a route that
calls request.text(), request.json() or readJson(request) buffers the whole body, up to that limit, in memory:
with maxBodyBytes: 512 * 1024 * 1024, one request can hold 512 MiB. Read the body with a bound of its own:
readJsonLimited(request, maxBytes)reads JSON and checks it likereadJson(415 for another content type, 400 for invalid JSON, a__proto__key or nesting deeper than 64), but never reads more thanmaxBytes. It reads the body as a stream and stops at the bound; aContent-Lengthabove the bound is refused before the first byte.readTextLimited(request, maxBytes)is the same for text.apiHandler({ body, maxBodyBytes })gives anapiHandlerthe same bound.apiHandler({ bodyTooLargeMessage })names the message of the 413 when the schema or the handler reads a body over that bound.serverFn({ maxBodyBytes })sets the bound of one server function's call (see Limits).- A route that really takes a large upload reads
request.bodyas a stream and writes it where it belongs, without collecting it.
Above the bound they throw BodyTooLargeError (413), the same error the handler throws when the app limit is passed,
so the answer is the same. A maxBytes that is NaN or negative is a RangeError: an unusable bound never means
"unbounded" (pass Infinity if you mean it).
import { json, readJsonLimited, type RequestEvent } from "@shadow-js/dusk/server";
// The app allows 100 MiB for uploads elsewhere; this route takes a small JSON document.
export async function POST({ request }: RequestEvent) {
const settings = await readJsonLimited(request, 16 * 1024);
return json({ saved: true, settings });
}
In development Dusk also warns when an API route reads its body in full while the app limit is above 10 MiB, once
per route, with the route, the reader and the limit. It sees readJson(request) and text(), json(),
formData(), arrayBuffer() and blob() called on the request object the route receives. It does not see
request.clone(), a request you build from it, or a read of a framework endpoint (server functions, the navigation
gate, draft mode), and a read through readJsonLimited, readTextLimited or request.body never warns. The check
exists only in development: a production handler does not wrap the request or change it in any way. At 10 MiB or
less nothing is checked.