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(defaulthyper; applies to connections accepted after a reload). Withexperimental, HTTP/1 connections — plain TCP after thePRIsniff, TLS with ALPNhttp/1.1or none, kTLS — are served bycrates/scalws-http/src/h1server.rs; HTTP/2, TLS with other ALPN values and HTTP/3 keep their current implementations.crates/scalws-h1is an I/O-free codec: request heads (httparse), the body framing decision, chunked coding, response heads, a per-thread cachedDate.- 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
writevstraight 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 Continueis 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_requestandh1_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.