Users' guide
How a request is processed
This page follows one HTTP request through ScalWS, from the accepted connection to the logged response, and lists the status code returned at every step that can refuse it.
The order is fixed. A request refused at one step never reaches the later ones: a request over the per-client rate is not routed, a denied path never reaches the response cache, and no application code runs before every check has passed.
Error answers produced by ScalWS itself are short text/plain; charset=utf-8 bodies made of
the status code and its reason phrase, for example 404 Not Found.
Connection
These steps run once per connection. A refusal here closes the connection (or, for HTTP
parsing errors, answers and then closes it); no request reaches the pipeline, so nothing
appears in the access log. Each refusal is counted in scalws_rejections_total with the
reason given below (see Logging and metrics).
-
Accept and the global connection limit. Every listener shares one pool of
server.limits.max_connectionsslots (HTTP/3 connections included). When all are taken, the accept loop pauses until a connection closes; waiting connections stay in the kernel backlog. Counted once per pause with reasonconnection_limit. -
PROXY protocol (listeners with
proxy_protocol). A peer outsideserver.trusted_proxiesis closed at once (untrusted_proxy). A missing, malformed or late header (it must arrive withinheader_read_timeout) closes the connection (proxy_protocol). The address in the header becomes the connection’s client address. -
Connections per client address.
server.security.per_ip.max_connectionslimits open connections per source (IPv4 address, or IPv6/64). Over the limit the connection is closed at once (source_connection_limit). Connections from trusted proxies are not limited per source: a load balancer carries many clients. -
TLS handshake (TLS listeners). It must finish within
server.limits.tls_handshake_timeout(tls_handshake_timeout); a failed handshake is counted astls_handshake. The certificate is chosen from the SNI name, and a handshake fails when no certificate matches and the listener has nodefaultone: see Domains, routing and default certificates. ALPNh2selects HTTP/2;http/1.1or no ALPN selects HTTP/1.1. On plain TCP, a connection that starts with the HTTP/2 preface (PRI) is served as HTTP/2 with prior knowledge, anything else as HTTP/1.1; the first bytes must arrive withinheader_read_timeout(header_timeout). HTTP/3 (QUIC) handshakes have the same bound (tls_handshake_timeout). A QUIC handshake from an address that has not been validated yet holds a connection permit only while such handshakes, over all HTTP/3 listeners of the instance, use less than a quarter ofmax_connections, while fewer than that many started within the lasttls_handshake_timeout, and only while its source’s open connections and unvalidated handshakes stay below the per-source limit (step 3); otherwise it is answered with a Retry, which only a client that receives at its address can follow, so forged source addresses cannot use up the permits. A client that follows the Retry comes back validated and is admitted or refused at once, before any TLS work. The per-source slot is taken once the handshake has validated the address, so a forged address never takes another’s slots. -
HTTP parsing and limits.
- The request head is limited by
server.limits.max_header_bytesandserver.limits.max_headers. On HTTP/1 a head that is too large is answered with431, a malformed one with400, and the connection is closed (bad_request). On HTTP/2,max_header_bytesis the header list size limit. On HTTP/3 it is the field section size limit, and any frame but DATA that declares more thanmax_header_bytesplus 1 KiB resets its stream (H3_EXCESSIVE_LOAD) before it is read. server.limits.header_read_timeout: an HTTP/1 request head must be complete within this time, otherwise the connection is closed (header_timeout).server.limits.idle_timeoutcloses HTTP/1 and HTTP/2 connections without a request in flight;server.limits.write_timeoutcloses a connection whose writes make no progress; on HTTP/2 and HTTP/3 a response whose body makes no send progress (a client that grants no flow-control credit makes nothing to write) is ended alone, the connection’s other streams and requests going on (an HTTP/2 connection is closed when all its responses are stalled and no request is in service, or afteridle_timeoutwhen it has only such responses left); the body goes out in steps of 16 KiB, each of which must be sent within the timeout, and the response and the tenant’s concurrency slot are released.server.limits.h2_max_concurrent_streamsbounds concurrent streams per HTTP/2 (and HTTP/3) connection.
With
server.http1_server: experimental, HTTP/1 connections are served by ScalWS’s own HTTP/1 implementation (ADR-0046) with the same limits and the same answers (431,400, then close). - The request head is limited by
Request
Every request that was parsed enters the same pipeline, whatever the protocol (HTTP/1.1,
HTTP/2 or HTTP/3). It gets a request id first: 24 hexadecimal characters, returned in the
x-request-id response header and written to the access log.
| Step | Check | Answer |
|---|---|---|
| 1 | ACME HTTP-01 challenge | 200 with the key authorization |
| 2 | Client address and trusted proxies | (no answer) |
| 3 | Per-client request rate | 429 |
| 4 | CONNECT | 405 |
| 5 | URI length | 414 |
| 6 | Host header | 400 |
| 7 | Path normalisation | 400 |
| 8 | Host lookup | 404 |
| 9 | Strict SNI | 421 |
| 10 | Denied paths | 404 |
| 11 | Per-path rate limits | 429 |
| 12 | https_redirect | 301 |
| 13 | .htaccess | 403, redirects, 400, 500, … |
| 14 | Route selection | (no answer) |
| 15 | Route allow / deny | 403 |
| 16 | Route CORS preflight | 204 |
| 17 | Route basic authentication | 401 or 503 |
| 18 | Request body size (route max_body) | 413 or 400 |
| 19 | Tenant quotas | 429 or 503 |
| 20 | Route rewrite and request headers | 400, 404 |
| 21 | Fixed answers | the respond answer |
| 22 | Response cache | cached response |
| 23 | Handler (route request_timeout) | the application’s answer, 504 |
| 24 | .htaccess error documents and headers | (changes the answer) |
| 25 | Response headers, metrics, access log | (route response_headers and CORS headers) |
In detail:
-
ACME HTTP-01 challenge. On a plain-HTTP connection,
GET /.well-known/acme-challenge/<token>for a token of a pending order is answered with200and the key authorization (application/octet-stream). This happens before the host check and beforehttps_redirect, so validation also works for applications that redirect to HTTPS. Unknown tokens and other/.well-known/paths continue through the pipeline to the application. See tls.md andserver.acme. -
Client address. When the connection’s peer is in
server.trusted_proxies, the client is the right-mostX-Forwarded-Forentry that is not itself a trusted proxy; if that entry cannot be parsed, the client is unknown (::), never the proxy. From any other peer,X-Forwarded-Foris ignored and the peer is the client. This address is used by every later check (rate limits,.htaccessRequire ip), the access log and the handlers (ADR-0022). -
Per-client request rate.
server.security.per_ip.requests_per_secondandburst: a token bucket per client address (IPv6 per/64). Over the rate:429withRetry-After(whole seconds, at least 1). Counted inscalws_security_blocked_totalwithreason="ip_rate". -
CONNECTis refused with405: ScalWS is not a forward proxy. -
URI length. A path and query longer than
server.limits.max_uri_bytesgets414. -
Host. The host is taken from the request target when it carries one (absolute-form URI, HTTP/2 and HTTP/3
:authority), otherwise from theHostheader.400when there is no host, when there is more than oneHostheader, whenHostdiffers from the request target’s authority, or when the host is not a valid name or address (see domains.md). -
Path normalisation. The path is percent-decoded,
.and..segments are resolved and repeated slashes collapsed.400for a path that does not start with/, an invalid percent escape, an encoded NUL byte, or..above the root. All later checks and route matching use the normalised path. -
Host lookup. The normalised host is looked up among the
domainsof all applications: exact names first, then one-label wildcards. A host that no application serves gets404. See domains.md. -
Strict SNI. On a listener with
strict_sni: true, a request whose host differs from the TLS SNI name of its connection gets421 Misdirected Request; the client retries on a new connection. Connections without SNI are not affected. Counted withreason="misdirected". -
Denied paths.
security.deny_paths(and the rules of the application’s profile):404. A directory request is also refused when one of its index files is denied (/uploads/would run/uploads/index.php). Counted withreason="denied_path". -
Per-path rate limits.
security.rate_limits, per client address:429withRetry-After. Counted withreason="route_rate". -
HTTPS redirect. With
https_redirect: true, a plain-HTTP request gets301withLocation: https://<host><path and query>. The host is the normalised one and no port is kept. -
.htaccess(applications withhtaccess: true, ADR-0031). Access rules answer403;RedirectandRewriteRulewith[R]answer with their redirect status andLocation; flags such as[F]and[G]answer with their status; a rewrite to an invalid path answers400and a rewrite loop500. A rewritten path is checked against the denied paths again (404). Otherwise the request continues with the rewritten path and query. See applications.md. -
Route selection on the normalised path (after
.htaccessrewrites):exactroutes first, thenprefixroutes (longest first), thenregexroutes in the order they are written; a route whose method, header or query matchers do not match is skipped. Requests matched by no route use the application’sruntime, or get404when it has none. See configuration.md. Steps 15 to 20 apply the policies of the selected route (route policies, ADR-0050); a route without policies skips them. When.htaccessrewrote the request, the route the client’s own path selects keeps itsallow/deny(step 15, both routes’ must pass) and itsbasic_auth(step 17, checked instead of the selected route’s). -
Route address lists. A client address (after trusted proxies) in the route’s
deny, or outside itsallowwhenallowis set, gets403. Counted withreason="route_access". -
CORS preflight. On a route with
cors, anOPTIONSrequest withOriginandAccess-Control-Request-Methodis answered with204and the CORS headers (only for an allowed origin); it never reaches the runtime. -
Basic authentication. On a route with
basic_auth, missing or wrong credentials get401withWWW-Authenticate(counted withreason="basic_auth"); when 64 password checks are already running,503withRetry-After: 1(reason="basic_auth_busy"). Accepted credentials are removed: the runtime never sees theAuthorizationheader. -
Request body size. A
Content-Lengthabove the route’smax_body, or elseserver.limits.max_request_body_bytes, gets413before any body byte is read; an invalidContent-Lengthgets400. A body without a length is counted while it streams: it is cut off at the limit, and a gap longer thanserver.limits.body_read_timeoutbetween two chunks ends it. The proxy and PHP handlers then answer413or408. A route’smax_bodymay be larger or smaller than the global limit. -
Tenant quotas.
requests_per_second(withburst) answers429withRetry-After;max_concurrent_requestsanswers503withRetry-After: 1when all slots are in use. Counted inscalws_tenant_limited_totalwithreasonrateorconcurrency. A concurrency slot is held until the response body is finished. -
Route rewrite and request headers. The route’s
rewritereplaces the path (and maybe the query) and sets the upstreamHost; a capture that would form a.or..segment, or a result above/, gets400. The rewritten path is checked against the denied paths again (404,reason="denied_path"); per-path rate limits and.htaccessare not run again. Then the route’srequest_headersare applied (by proxies after theirX-Forwarded-*headers; for every other runtime, PHP included, here). -
Fixed answers. In an application without
.htaccess, arespondruntime answers here directly. -
Response cache (with
server.cacheand the application’scache). A fresh entry is answered from the cache; otherwise the handler runs and its response may be stored.respondroutes never use the cache, static files only with adefault_ttl, and routes withbasic_auth,allowordenynever. When the application’s routes are not all plain prefixes, entries are kept per route. The answer carriesx-cache: HIT,STALE,MISSorBYPASS. See caching.md. -
Handler.
static:405(withAllow: GET, HEAD) for other methods,404for missing or denied files,403when the file cannot be read,301to add the trailing slash of a directory,304,206and416for conditional and range requests.proxy,php,node,python,container: the application’s answer, or502when the upstream cannot be reached or answers with an invalid response,504on a response timeout,503withRetry-After: 1when no worker is ready or an upstream pool has no member. These are counted inscalws_upstream_failures_total. A pool may retry on another member first (load-balancing.md).respondandredirect: their configured answer.
A route’s
request_timeoutbounds steps 22 and 23 until the response head:504when it passes. The response body is not bounded. See applications.md. -
.htaccessafter the handler. AnErrorDocumentfor the response status replaces the body (a local document keeps the original status, a URL redirects with302), andHeaderdirectives are applied. -
Response.
Server: scalwsis added unless the handler set its ownServerheader, thenx-request-id; then the matched route’sresponse_headers(also on answers ScalWS generated in steps 15 to 23) and, on a route withcors, its CORS headers andVary: Origin.Alt-Svcis added on TLS listeners that also serve HTTP/3. Request metrics are recorded when the response head is ready. The access log line (withserver.logging.access_log) and the per-domain byte counters are written when the response body is finished, or when it is dropped because the client went away. See Logging and metrics.
Requests refused in steps 1 to 8 have no application yet: they appear in metrics and in
the access log with tenant and application _unmatched.
See also
- security.md: limits, rate limits, trusted proxies and a hardening checklist.
- domains.md: host matching and certificate selection.
- configuration.md: route matching and route policies.
- load-balancing.md: upstream pools, retries and passive health.
- logging-metrics.md: the access log and every metric.
- troubleshooting.md: common errors and their causes.