RinhaDeBackend::HttpServer
Minimal HTTP/1.1 server tailored to the Rinha workload: two known
routes (POST /fraud-score, GET /ready), tiny POST bodies, no
query string, no chunked encoding, no upgrades.
Why we replaced HTTP::Server:
- per-request
Request/Response/headerHashallocations, - 8 KB
IO::Bufferedread+write buffers per kept-alive connection, - keep-alive race that occasionally RSTs sockets under load.
Design here:
- One fiber per connection (same as stdlib).
- Per-fiber 8 KB stack buffer (uninitialized StaticArray) — survives I/O yields, costs nothing to "allocate".
- HttpParser (pure Crystal) parses the request line + headers from the same stack buffer, zero allocations on the hot path.
- Six pre-rendered fraud-score responses + three static
status-only responses, written with a single
socket.write. read_buffering = false,sync = true,tcp_nodelay = true: no internal IO::Buffered allocation per connection, no Nagle.- Keep-alive by default for HTTP/1.1; only
Connection: closeforces shutdown (or HTTP/1.0 unlessConnection: keep-alive).
Constants
8 KB is overkill for the Rinha schema (request bodies fit in <1 KB, request lines + headers fit in ~600 B). Headroom is cheap since the buffer is on the fiber stack.
Lowercase reference values for case-insensitive header matching.
Six possible outcomes (frauds_in_top_5 = 0..5). Threshold is 0.6, so 3+ frauds → not approved. The bodies match what FraudScoreAction had before; we just inline the HTTP envelope so the whole response can be written in a single syscall, no streaming, no header serialization.
Constructors
Either listen on TCP (host/port) or on a Unix Domain Socket path.
UDS mode is selected by passing a non-nil uds_path; in that mode
host/port are ignored. The prod LB (HAProxy, see haproxy.cfg)
talks to the API exclusively over UDS — TCP mode is kept as the
dev/local path so specs/tools that don't go through the LB still
work.
Class methods
Parses the request envelope from buf using HttpParser, then
returns {method, path, body, connection_close} with byte slices
pointing into buf. Returns nil on partial/invalid input. Used by
specs to exercise the full parse path without TCP.
Header parsing helpers. Public class methods so spec/http_server_spec can exercise them with synthetic phr_header input — these are the pieces that bit us last time (Content-Length came back as 0, silent wrong answer downstream).
Instance methods
Listen with an optional post-bind / pre-accept callback. The
callback runs after the server socket is bound (so the docker
healthcheck test -S /sockets/api*.sock already passes) and
before the accept loop starts (so the caller can do heavy
warm-up like References#prefault! without delaying the
healthcheck). The kernel queues incoming UDS connections in the
backlog while the callback runs; the LB itself doesn't start
until the API healthcheck passes, so by the time real traffic
arrives the accept loop is already up.