Decision records

ADR-0046: Own HTTP/1 server side (opt-in)

Status: accepted as an opt-in experiment — 2026-10-09

Context

ScalWS stays behind nginx on the bare reverse proxy and by a few percent on small static files. Profiling put the remaining cost in user space (hyper’s HTTP/1 dispatcher, body channel, header handling, tokio scheduling), not in the kernel. With ADR-0045 the upstream client is our own; the server side was still hyper. The owner allowed writing HTTP/1 framing and the server side ourselves, on condition that it is secure and audited.

Decision

  • server.http1_server: hyper | experimental (default hyper; applies to connections accepted after a reload). With experimental, HTTP/1 connections — plain TCP after the PRI sniff, TLS with ALPN http/1.1 or none, kTLS — are served by crates/scalws-http/src/h1server.rs; HTTP/2, TLS with other ALPN values and HTTP/3 keep their current implementations.
  • crates/scalws-h1 is an I/O-free codec: request heads (httparse), the body framing decision, chunked coding, response heads, a per-thread cached Date.
  • Framing is stricter than hyper where two parsers could disagree: TE with CL, repeated TE, TE on HTTP/1.0, any coding other than exactly chunked, Content-Length lists or non-decimal values → 400 and close.
  • One task per connection; the service future is polled by the connection; bodies that arrived with the head need no streaming; other bodies are read only on demand (64 KiB or 256 frames); responses are written with writev straight from their buffers (sendfile placeholders unchanged), bounded at 256 KiB.
  • Behaviour kept from hyper: a final 1xx is answered 500 and the connection closes; a client that goes away cancels its request in flight; 100 Continue is sent only before the final head; abandoned bodies are drained up to 64 KiB without waiting, otherwise the connection closes.
  • Upgrades use scalws_core::upgrade::OnUpgrade (the proxy accepts it and hyper’s).

Validation

  • Differential test against hyper over ~365 raw request streams (tests/integration/tests/h1_differential.rs): identical except three intended, stricter answers.
  • The whole integration suite runs against both servers (SCALWS_TEST_HTTP1_SERVER).
  • Fuzz targets h1_request and h1_connection (whole connections over an in-memory socket, checking that every request reaching the handler gets exactly one response).
  • Security audit run-2 (scoped, standard profile): five findings, all fixed with regression tests — see docs/hardening/2026-10-09-own-http1-audit.md.

Measurements

First A/B (docs/benchmarks/results/2026-10-09-own-h1-ab): server CPU per request −2 % to −10 % across static, respond, proxy, cache hits and PHP; throughput within noise. It stays opt-in until it wins on throughput against nginx and has passed a long soak.