[]previous and next page

The server core: rules, bodies, caches and streams

These rules apply after the adapter receives a request.

For agentsUpdated

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.