# How a request is processed

URL: https://scalws.com/docs/guide/request-processing/
Source: `docs/guide/request-processing.md` in the ScalWS repository

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](https://scalws.com/docs/guide/logging-metrics/)).

1. **Accept and the global connection limit.** Every listener shares one pool of
   [`server.limits.max_connections`](https://scalws.com/docs/reference/server-limits/#server.limits.max_connections)
   slots (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 reason `connection_limit`.
2. **PROXY protocol** (listeners with
   [`proxy_protocol`](https://scalws.com/docs/reference/server-listeners/#server.listeners-.proxy_protocol)).
   A peer outside [`server.trusted_proxies`](https://scalws.com/docs/reference/server/#server.trusted_proxies)
   is closed at once (`untrusted_proxy`). A missing, malformed or late header (it must
   arrive within `header_read_timeout`) closes the connection (`proxy_protocol`). The
   address in the header becomes the connection's client address.
3. **Connections per client address.**
   [`server.security.per_ip.max_connections`](https://scalws.com/docs/reference/server-security/#server.security.per_ip.max_connections)
   limits 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.
4. **TLS handshake** (TLS listeners). It must finish within
   [`server.limits.tls_handshake_timeout`](https://scalws.com/docs/reference/server-limits/#server.limits.tls_handshake_timeout)
   (`tls_handshake_timeout`); a failed handshake is counted as `tls_handshake`. The
   certificate is chosen from the SNI name, and a handshake fails when no certificate
   matches and the listener has no `default` one: see
   [Domains, routing and default certificates](https://scalws.com/docs/guide/domains/). ALPN `h2` selects HTTP/2;
   `http/1.1` or 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 within `header_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 of `max_connections`, while fewer than that many started within the last
   `tls_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.
5. **HTTP parsing and limits.**
   * The request head is limited by
     [`server.limits.max_header_bytes`](https://scalws.com/docs/reference/server-limits/#server.limits.max_header_bytes)
     and [`server.limits.max_headers`](https://scalws.com/docs/reference/server-limits/#server.limits.max_headers).
     On HTTP/1 a head that is too large is answered with `431`, a malformed one with `400`,
     and the connection is closed (`bad_request`). On HTTP/2, `max_header_bytes` is the
     header list size limit. On HTTP/3 it is the field section size limit, and any frame
     but DATA that declares more than `max_header_bytes` plus 1 KiB resets its stream
     (`H3_EXCESSIVE_LOAD`) before it is read.
   * [`server.limits.header_read_timeout`](https://scalws.com/docs/reference/server-limits/#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_timeout`](https://scalws.com/docs/reference/server-limits/#server.limits.idle_timeout)
     closes HTTP/1 and HTTP/2 connections without a request in flight;
     [`server.limits.write_timeout`](https://scalws.com/docs/reference/server-limits/#server.limits.write_timeout)
     closes 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 after `idle_timeout` when 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_streams`](https://scalws.com/docs/reference/server-limits/#server.limits.h2_max_concurrent_streams)
     bounds concurrent streams per HTTP/2 (and HTTP/3) connection.

   With [`server.http1_server: experimental`](https://scalws.com/docs/reference/server/#server.http1_server),
   HTTP/1 connections are served by ScalWS's own HTTP/1 implementation
   ([ADR-0046](https://scalws.com/docs/adr/0046-own-http1-server/)) with the same limits and the same answers
   (`431`, `400`, then close).

## 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:

1. **ACME HTTP-01 challenge.** On a plain-HTTP connection, `GET
   /.well-known/acme-challenge/<token>` for a token of a pending order is answered with
   `200` and the key authorization (`application/octet-stream`). This happens before the
   host check and before `https_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](https://scalws.com/docs/guide/tls/) and
   [`server.acme`](https://scalws.com/docs/reference/server/#server.acme).
2. **Client address.** When the connection's peer is in
   [`server.trusted_proxies`](https://scalws.com/docs/reference/server/#server.trusted_proxies), the client is
   the right-most `X-Forwarded-For` entry 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-For` is ignored and the peer is the client. This address is used by every
   later check (rate limits, `.htaccess` `Require ip`), the access log and the handlers
   ([ADR-0022](https://scalws.com/docs/adr/0022-trusted-proxies/)).
3. **Per-client request rate.**
   [`server.security.per_ip.requests_per_second`](https://scalws.com/docs/reference/server-security/#server.security.per_ip.requests_per_second)
   and [`burst`](https://scalws.com/docs/reference/server-security/#server.security.per_ip.burst): a token
   bucket per client address (IPv6 per `/64`). Over the rate: `429` with `Retry-After` (whole
   seconds, at least 1). Counted in `scalws_security_blocked_total` with `reason="ip_rate"`.
4. **`CONNECT`** is refused with `405`: ScalWS is not a forward proxy.
5. **URI length.** A path and query longer than
   [`server.limits.max_uri_bytes`](https://scalws.com/docs/reference/server-limits/#server.limits.max_uri_bytes)
   gets `414`.
6. **Host.** The host is taken from the request target when it carries one (absolute-form
   URI, HTTP/2 and HTTP/3 `:authority`), otherwise from the `Host` header. `400` when there
   is no host, when there is more than one `Host` header, when `Host` differs from the
   request target's authority, or when the host is not a valid name or address (see
   [domains.md](https://scalws.com/docs/guide/domains/#normalisation)).
7. **Path normalisation.** The path is percent-decoded, `.` and `..` segments are resolved
   and repeated slashes collapsed. `400` for 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.
8. **Host lookup.** The normalised host is looked up among the
   [`domains`](https://scalws.com/docs/reference/applications/#tenants-.applications-.domains) of all
   applications: exact names first, then one-label wildcards. A host that no application
   serves gets `404`. See [domains.md](https://scalws.com/docs/guide/domains/).
9. **Strict SNI.** On a listener with
   [`strict_sni: true`](https://scalws.com/docs/reference/server-listeners/#server.listeners-.strict_sni), a
   request whose host differs from the TLS SNI name of its connection gets
   `421 Misdirected Request`; the client retries on a new connection. Connections without
   SNI are not affected. Counted with `reason="misdirected"`.
10. **Denied paths.**
    [`security.deny_paths`](https://scalws.com/docs/reference/applications/#tenants-.applications-.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 with `reason="denied_path"`.
11. **Per-path rate limits.**
    [`security.rate_limits`](https://scalws.com/docs/reference/applications/#tenants-.applications-.security.rate_limits),
    per client address: `429` with `Retry-After`. Counted with `reason="route_rate"`.
12. **HTTPS redirect.** With
    [`https_redirect: true`](https://scalws.com/docs/reference/applications/#tenants-.applications-.https_redirect),
    a plain-HTTP request gets `301` with `Location: https://<host><path and query>`. The
    host is the normalised one and no port is kept.
13. **`.htaccess`** (applications with
    [`htaccess: true`](https://scalws.com/docs/reference/applications/#tenants-.applications-.htaccess),
    [ADR-0031](https://scalws.com/docs/adr/0031-htaccess/)). Access rules answer `403`; `Redirect` and
    `RewriteRule` with `[R]` answer with their redirect status and `Location`; flags such
    as `[F]` and `[G]` answer with their status; a rewrite to an invalid path answers `400`
    and a rewrite loop `500`. A rewritten path is checked against the denied paths again
    (`404`). Otherwise the request continues with the rewritten path and query. See
    [applications.md](https://scalws.com/docs/guide/applications/#htaccess).
14. **Route selection** on the normalised path (after `.htaccess` rewrites): `exact`
    routes first, then `prefix` routes (longest first), then `regex` routes 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's
    [`runtime`](https://scalws.com/docs/reference/applications/#tenants-.applications-.runtime), or get `404`
    when it has none. See [configuration.md](https://scalws.com/docs/guide/configuration/#route-matching). Steps 15 to
    20 apply the policies of the selected route
    ([route policies](https://scalws.com/docs/guide/configuration/#route-policies),
    [ADR-0050](https://scalws.com/docs/adr/0050-route-matching-and-policies/)); a route without policies skips
    them. When `.htaccess` rewrote the request, the route the client's own path selects
    keeps its `allow`/`deny` (step 15, both routes' must pass) and its `basic_auth` (step
    17, checked instead of the selected route's).
15. **Route address lists.** A client address (after trusted proxies) in the route's
    `deny`, or outside its `allow` when `allow` is set, gets `403`. Counted with
    `reason="route_access"`.
16. **CORS preflight.** On a route with `cors`, an `OPTIONS` request with `Origin` and
    `Access-Control-Request-Method` is answered with `204` and the CORS headers (only for
    an allowed origin); it never reaches the runtime.
17. **Basic authentication.** On a route with `basic_auth`, missing or wrong credentials
    get `401` with `WWW-Authenticate` (counted with `reason="basic_auth"`); when 64 password
    checks are already running, `503` with `Retry-After: 1` (`reason="basic_auth_busy"`).
    Accepted credentials are removed: the runtime never sees the `Authorization` header.
18. **Request body size.** A `Content-Length` above the route's `max_body`, or else
    [`server.limits.max_request_body_bytes`](https://scalws.com/docs/reference/server-limits/#server.limits.max_request_body_bytes),
    gets `413` before any body byte is read; an invalid `Content-Length` gets `400`. A body
    without a length is counted while it streams: it is cut off at the limit, and a gap
    longer than
    [`server.limits.body_read_timeout`](https://scalws.com/docs/reference/server-limits/#server.limits.body_read_timeout)
    between two chunks ends it. The proxy and PHP handlers then answer `413` or `408`. A
    route's `max_body` may be larger or smaller than the global limit.
19. **Tenant quotas.**
    [`requests_per_second`](https://scalws.com/docs/reference/tenants/#tenants-.resources.requests_per_second)
    (with [`burst`](https://scalws.com/docs/reference/tenants/#tenants-.resources.burst)) answers `429` with
    `Retry-After`;
    [`max_concurrent_requests`](https://scalws.com/docs/reference/tenants/#tenants-.resources.max_concurrent_requests)
    answers `503` with `Retry-After: 1` when all slots are in use. Counted in
    `scalws_tenant_limited_total` with `reason` `rate` or `concurrency`. A concurrency slot
    is held until the response body is finished.
20. **Route rewrite and request headers.** The route's `rewrite` replaces the path (and
    maybe the query) and sets the upstream `Host`; a capture that would form a `.` or `..`
    segment, or a result above `/`, gets `400`. The rewritten path is checked against the
    denied paths again (`404`, `reason="denied_path"`); per-path rate limits and
    `.htaccess` are not run again. Then the route's `request_headers` are applied (by
    proxies after their `X-Forwarded-*` headers; for every other runtime, PHP included,
    here).
21. **Fixed answers.** In an application without `.htaccess`, a `respond` runtime answers
    here directly.
22. **Response cache** (with [`server.cache`](https://scalws.com/docs/reference/server/#server.cache) and the
    application's [`cache`](https://scalws.com/docs/reference/applications/#tenants-.applications-.cache)).
    A fresh entry is answered from the cache; otherwise the handler runs and its response
    may be stored. `respond` routes never use the cache, static files only with a
    `default_ttl`, and routes with `basic_auth`, `allow` or `deny` never. When the
    application's routes are not all plain prefixes, entries are kept per route. The
    answer carries `x-cache: HIT`, `STALE`, `MISS` or `BYPASS`. See [caching.md](https://scalws.com/docs/guide/caching/).
23. **Handler.**
    * `static`: `405` (with `Allow: GET, HEAD`) for other methods, `404` for missing or
      denied files, `403` when the file cannot be read, `301` to add the trailing slash of
      a directory, `304`, `206` and `416` for conditional and range requests.
    * `proxy`, `php`, `node`, `python`, `container`: the application's answer, or `502`
      when the upstream cannot be reached or answers with an invalid response, `504` on a
      response timeout, `503` with `Retry-After: 1` when no worker is ready or an upstream
      pool has no member. These are counted in `scalws_upstream_failures_total`. A pool
      may retry on another member first ([load-balancing.md](https://scalws.com/docs/guide/load-balancing/#retries)).
    * `respond` and `redirect`: their configured answer.

    A route's `request_timeout` bounds steps 22 and 23 until the response head: `504`
    when it passes. The response body is not bounded. See
    [applications.md](https://scalws.com/docs/guide/applications/#runtimes).
24. **`.htaccess` after the handler.** An `ErrorDocument` for the response status replaces
    the body (a local document keeps the original status, a URL redirects with `302`), and
    `Header` directives are applied.
25. **Response.** `Server: scalws` is added unless the handler set its own `Server`
    header, then `x-request-id`; then the matched route's `response_headers` (also on
    answers ScalWS generated in steps 15 to 23) and, on a route with `cors`, its CORS
    headers and `Vary: Origin`. `Alt-Svc` is added on TLS listeners that also serve
    [HTTP/3](https://scalws.com/docs/reference/server-listeners/#server.listeners-.http3). Request metrics are
    recorded when the response head is ready. The access log line (with
    [`server.logging.access_log`](https://scalws.com/docs/reference/server-logging/#server.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](https://scalws.com/docs/guide/logging-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](https://scalws.com/docs/guide/security/): limits, rate limits, trusted proxies and a hardening checklist.
* [domains.md](https://scalws.com/docs/guide/domains/): host matching and certificate selection.
* [configuration.md](https://scalws.com/docs/guide/configuration/#route-matching): route matching and route policies.
* [load-balancing.md](https://scalws.com/docs/guide/load-balancing/): upstream pools, retries and passive health.
* [logging-metrics.md](https://scalws.com/docs/guide/logging-metrics/): the access log and every metric.
* [troubleshooting.md](https://scalws.com/docs/guide/troubleshooting/): common errors and their causes.
