The request pipeline
Every request for served content runs through one ordered pipeline. Each stage is driven by the site’s config, and the stages run in a fixed order so the behavior is predictable. Nothing is buffered whole in memory — the response streams from the backend as soon as the pipeline resolves it.
The order
- Host → site. The
Hostheader selects the site (virtualhost routing), with an optional default site for an unmatched host. The full set of ways a request is matched to a site is in How a request reaches your site. - Transport. HTTPS redirect and HSTS, proxy-aware through
X-Forwarded-Protofrom a trusted proxy. - Access control. WAF, then IP rules, then rate limit, then basic auth — the first to reject wins. See Restrict visitor access.
- Path normalization. Clean URLs, the trailing-slash policy, and dot-segment collapsing (traversal-safe).
- Route. Redirects, then handlers, then rewrites / SPA fallback / reverse-proxy.
- Resolve. Map the path to a manifest entry — a directory index, or a custom error document when nothing matches.
- HTTP correctness. Conditional
304,Range/206,ETag, response headers,Cache-Control, and compression negotiation.
An early stage can end the request — a rejected access-control check, a redirect, a handler that answers — before the later stages run.
Why the order is fixed
The order encodes precedence you would otherwise have to reason about per
request. Access control runs before any content work, so a blocked request never
touches the manifest. Redirects run before handlers, so a moved path does not
invoke code. Path normalization runs before routing, so route patterns match a
canonical path and cannot be bypassed with .. or a double slash.
The routing core is pure and shared
Stages 4 through 7 — normalization, routing, resolution, and HTTP correctness —
are pure functions in boatramp_core::route, with no I/O. That has two
consequences. They are unit-tested in isolation, against inputs rather than a
running server. And they are reused by the Cloudflare edge Worker, so a request
routes identically at the edge and at the origin — the two cannot drift, because
they run the same code. See the architecture overview.