How it works · one request, step by step
How a request travels
A request does not wander through a framework. It passes a short, fixed list of checks, in this order, and the first check it fails ends it with a plain status. Nothing later runs. This page is that list, with the status each step answers with when it says no.
§ 01 · Every request
Five questions, in order
Pages, actions and files all start here.
-
Is it too big?
A body larger than the limit (1 MiB) is refused before anything else happens. The declared length is checked first; a body with no length is read up to the limit.
→ 413 Also: request line and headers together over 16 KiB get 431, as a bare status. -
Is the path sane?
A path with a .. or . segment, an empty segment (//), a backslash, a control character or invalid UTF-8 is refused, once, up front. No later code ever sees such a path.
→ 404 -
Is there a route?
Routes come from [routes] in dhtml.config and from files. Config beats files. A static name beats a parameter: /blog/new reaches blog/new.dhtml before blog/[slug].dhtml. A page answers GET and HEAD only.
→ 405 + Allow→ 404 Two files that serve the same URL fail the check, so the upload is refused. -
Is it a file in public/?
For GET and HEAD, the file is opened through os.Root, which cannot leave public/. Content types come from a fixed table, not the operating system. Symlinks are never served. Anything under server/ or bin/, dhtml.config, any name starting with a dot (except /.well-known/) and any .dhtml file is never served, whatever is on disk.
-
Otherwise: nothing here
A missing file, a blocked file and a bad name all look the same from outside. The visitor sees the status text and nothing else.
→ 404
§ 02 · An action
Six more, for a form post
A file in server/api/ becomes an action. It answers one method.
-
Is it the right method?
One action file answers the one method its <d:action> declares. A GET action also answers HEAD.
→ 405 + Allow -
Is the visitor allowed?
auth and admin actions answer 401 to a visitor who is not signed in, and admin actions 403 to one who is not an admin. The session cookie holds 32 random bytes; the database keeps only their SHA-256.
→ 401→ 403 -
Is it too often?
ratelimit="10/minute" counts requests per action and client address, and for a signed-in visitor per account as well, so a new address is not a new allowance. The counters are a file, server/data/ratelimit.db. If the counter cannot be used the answer is 503, not a free pass.
→ 429 + Retry-After→ 503 -
Did the right page send it?
For every method but GET: a cross-site Origin or Sec-Fetch-Site is refused, and so is a missing or wrong CSRF token. The token is tied to the session while there is one, so one planted by another site does not work. A token in the X-CSRF-Token header is checked before the body is read; in a multipart form the _csrf field must come first and is judged before anything else is read.
→ 403 -
Is the body acceptable?
Form-encoded or multipart, nothing else. The size is the global limit plus the sizes of the action's declared file fields. Only declared fields are kept: a visitor who adds is_admin to a form sends a field that no longer exists.
→ 415→ 413 -
Validation, steps, one response
The <d:validate> rules run first; a failure draws the action's <d:invalid> with the values escaped. Then the steps run top to bottom: queries, writes, <d:set>, <d:http>, <d:ai>. An action with a write is wrapped in one transaction that commits only when the action ends in a response that is not an error. Every path ends in exactly one response, or the project does not compile.
→ 422→ 500 (details in the log) Responses carry Cache-Control: no-store.
§ 03 · A page
What a page does
Pages are read-only by construction.
Before a new version goes live, every page, layout and partial is compiled, and the upload is refused if one fails.
Compiling turns the page into one template. That is what knows whether a value lands in text, an attribute, a URL, a script or a style, and escapes it for that place.
Queries run in order, before anything is drawn. A page can only read: a query may return at most 10,000 rows, and all the queries of one request share a 10-second limit. Each {{ }} in the SQL is sent as a separate value, never pasted into the text; a value cannot name a table or a column.
Then the page is drawn, the answer is 200 text/html with Cache-Control: no-cache, with the security headers. A page can stop at any point with <d:status code="404" />.
If a page fails while drawing, a visitor gets a bare 500. The log gets one line with the file, the line and the reason, and never the query string.
