The server core: rules, bodies, caches and streams
These rules apply after the adapter receives a request.
These rules apply after the adapter receives a request.
Rules cover every spelling the router serves. The redirects, rewrites, headers and routeRules of dusk.config.ts
match like the router: segments are decoded once, the literal parts compare without ASCII case unless router.caseSensitive is
on, a dot segment is resolved by the URL parser. A header rule for /account/** therefore also covers /ACCOUNT/data and
/%61ccount/data. A *name rest never matches a path whose rest holds an empty, . or .. segment, so
/docs/..%2F_private%2Fx is not rewritten into /content/../_private/x and a destination that starts with the splat cannot
become //host. The patterns are not regular expressions and match in time linear in the path.
Authorization belongs to the route. guard, a route's middleware and a _middleware folder follow the route the router
picked, so they run for every spelling. App-wide middleware that compares event.url.pathname with a string sees the spelling the
client sent (/ADMIN, /%61dmin), which the router still serves: do not use such a check as the only gate.
Bodies are decoded strictly. Text and JSON that are not valid in their charset are a 400, never U+FFFD, so two byte strings
cannot become one key or one value. JSON is UTF-8 only (a charset of utf-16le, latin1 or utf-7 is 415). A form field name
that carries U+FFFD (the sign of bytes that were not UTF-8) is a 400. A multipart or urlencoded body that does not parse is a 400,
also for a form: true server function and the draft endpoint. A compressed body is counted in decoded bytes, so a bomb is a 413
after a few chunks. __proto__, constructor and prototype field names are dropped, and the result has no prototype.
Cookies and draft mode. A signed cookie has one valid spelling (the signature must be the canonical base64url), the signature
binds the cookie name, and every listed secret verifies, so rotation is: new secret first, old secret kept until its cookies
expire. The draft cookie is the exception: it is signed with the one draft secret only and carries its own expiry (8 hours, also
its Max-Age), so changing DUSK_DRAFT_SECRET revokes every draft cookie and a copied cookie dies on schedule. The secret is
compared by digest. The enable endpoint is a POST with a body secret, and the app-wide middleware runs before it: put a rate limit
there if the secret could be guessed.
The page cache keys on the page, and renders only what the key holds. The key is host, canonical path, the query parameters
in cache.searchParams (all of them when none are named) and the declared cookies. The render for the cache sees only those
parameters. It records the request headers the page reads: a page that read one its Vary does not name is answered for that
request and not stored, with a development warning. Name a header you vary on by setting vary in the loader; do not read
identity from a header (X-Forwarded-User) in a cached route. /About and /about are one entry, so revalidatePath removes it
whichever spelling filled it. There is no HTTP endpoint that purges the cache.
The data cache keys on what changes the answer. cachedFetch includes the request headers in the key (as a digest, so a
header value never reaches the store) and refuses a URL with a user name or password. It still refuses authorization, cookie
and proxy-authorization: it is for public data. defineLocals copies an array, plain object, Set, Map or Date default
for each request; a class instance or function default is shared as it is.
Streams cannot be made to hold unbounded memory by a peer that does not read. stream.comment() and the heartbeat are
dropped while a chunk waits (a comment is at most 1 KiB), send is bounded by streams.sseMaxBufferedEvents, and a WebSocket
transport that holds more than 8 times wsMaxMessageBytes of unsent bytes is dropped with 1013. A frame that arrives a byte at a
time costs one buffer. The decision of an upgrade (app-wide middleware and WS.upgrade) is bounded by limits.requestTimeoutMs.
The Node upgrade is built from the same checks as a plain request: one well-formed Host, an origin-form target.
after() runs after the answer. Without waitUntil the tasks start when the body has been read to its end (or the client
canceled it); with waitUntil, one promise is handed over and a waitUntil that throws is reported without touching the answer.
A failing task, a throwing error sink and a rejecting onRequestError raise no unhandled rejection and put nothing in the
response.
Response headers and files. useResponse refuses a name or value with CR, LF, NUL, a space or a colon, and refuses
set-cookie, location, content-length and transfer-encoding; cookies go through setCookie (which validates the name,
path and domain and defaults to HttpOnly; Secure; SameSite=Lax). event.responseHeaders is the raw Headers behind it and does not
check a set-cookie: use event.cookies. file() names the download as a leaf (/ and \ become _), writes filename*
(RFC 6266) beside an ASCII fallback for a non-ASCII or % name, and refuses CR and LF.