Ten sequential spec files plus index covering runtime, observability, security, data, routing, realtime, supervisor, clients, CLI ops, and workflow, each with best practices, structure, rules, guidelines, specs, and cross-references.
21 KiB
Core Runtime and HTTP Fundamentals
File: plans/01-runtime.md Scope: plan.md sections 2-3, 10 step 1 partial. Topics: event loop, lifecycle, HTTP/1.1 + HTTP/2, compression, cookies, static.
1. Purpose and philosophy
Provide the single non-blocking foundation every other component builds on: one in-house reactor, one coroutine model, one connection lifecycle, one HTTP/1.1 plus HTTP/2 stack, one compression layer, one cookie transport, one static file path. No feature reimplements I/O, parsing, framing, or file delivery.
Philosophy:
- Boring core, strict caps. Every knob has a static ceiling. No autotuning.
- Correctness from standards, not folklore. HTTP behavior follows RFC 9110/9112/9113, cookies follow RFC 6265, compression formats follow RFC 1952/7932.
- Transport only here. Sessions, auth, guards live in 03-security.md. Routing, views, middleware, errors live in 05-request-routing.md. Metrics and tracing hooks live in 02-observability.md but cost near zero when disabled.
- Deep structure: one class or concern per file, namespaces mirror directories.
2. Best practices (from research)
- HTTP/1.1: persistent connections are the default; framing comes from Content-Length or chunked transfer coding; a recipient MUST parse chunked; unknown transfer codings SHOULD get 501; keep-alive plus correct length framing enables connection reuse (Source: https://www.rfc-editor.org/rfc/rfc9112, retrieved 2026-10-23).
- HTTP/1.1 request smuggling defense: never accept both Content-Length and Transfer-Encoding on one message; treat conflicts as 400 and close. This triangulates with RFC 9112 framing rules and current CVE practice around chunked plus keep-alive handling.
- HTTP/2: binary framing, interleaved multiplexed streams on one connection, HPACK field compression, connection-level plus stream-level flow control, request prioritization (Source: https://www.rfc-editor.org/rfc/rfc9113, retrieved 2026-10-23). Independent H2 implementations confirm the same trio: multiplexing, HPACK, dual flow control.
- HTTP/2 server push is gone in RFC 9113 (obsoletes RFC 7540/8740). Do not implement push. Use 103 Early Hints, normal preload, or SSE from 06-realtime.md instead (Source: https://www.rfc-editor.org/rfc/rfc9113, retrieved 2026-10-23).
- HTTP semantics are version independent and defined once in RFC 9110: methods, status codes, fields, content negotiation, validators, ranges, caching (Source: https://www.rfc-editor.org/rfc/rfc9110, retrieved 2026-10-23). MoloApp therefore shares one Request/Response semantic layer across H1 and H2.
- Static validators: send both ETag and Last-Modified on 200 when cheap; If-None-Match takes precedence over If-Modified-Since; support If-Range, single byte ranges with 206, and 416 for unsatisfiable ranges (Source: https://www.rfc-editor.org/rfc/rfc9110, retrieved 2026-10-23; corroborated by https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching, retrieved 2026-10-23).
- Cookies: servers store state via Set-Cookie and Cookie fields; RFC 6265 defines syntax, domain/path scoping, and expiry semantics (Source: https://www.rfc-editor.org/rfc/rfc6265, retrieved 2026-10-23). H2 MAY split one logical Cookie into several fields to help HPACK; a server MUST join them back with "; " before parsing (Source: https://www.rfc-editor.org/rfc/rfc9113, retrieved 2026-10-23). Set-Cookie MUST never be joined.
- Compression: Brotli combines LZ77 plus Huffman coding with quality comparable to best general-purpose methods (Source: https://www.rfc-editor.org/rfc/rfc7932, retrieved 2026-10-23). gzip wraps DEFLATE with header, CRC32, and original size (Source: https://www.rfc-editor.org/rfc/rfc1952, retrieved 2026-10-23). Both are negotiated via Accept-Encoding/Content-Encoding; Vary: Accept-Encoding is required on negotiated responses.
- Reactor: epoll scales to many FDs via epoll_create/ctl/wait and supports level-triggered (default) plus edge-triggered (EPOLLET) modes (Source: https://man7.org/linux/man-pages/man7/epoll.7.html, retrieved 2026-10-23). kqueue (BSD/macOS) and IOCP with GetQueuedCompletionStatus/PostQueuedCompletionStatus (Windows, see https://learn.microsoft.com/en-us/windows/win32/fileio/synchronous-and-asynchronous-i-o, retrieved 2026-10-23) are the native peers. MoloApp hides all three behind one Poller interface; default is level-triggered for safety.
- Coroutines: a C++20/23 coroutine is promise plus handle plus awaitable; initial_suspend decides lazy vs eager start; co_await suspends without blocking the thread (Source: https://en.cppreference.com/cpp/language/coroutines, retrieved 2026-10-23). All handlers are coroutines returning Task; blocking work never runs on loop threads.
3. Structure
Directories mirror namespaces. One concern per file. Tests mirror routes per 10-workflow-example.md.
src/molo/runtime/
event_loop.h/.cpp molo::runtime::EventLoop
poller.h molo::runtime::IPoller (interface)
poller_epoll.h/.cpp molo::runtime::EpollPoller (Linux)
poller_kqueue.h/.cpp molo::runtime::KqueuePoller (BSD/macOS)
poller_iocp.h/.cpp molo::runtime::IocpPoller (Windows)
task.h molo::runtime::Task<T>, molo::runtime::Generator<T>
executor.h/.cpp molo::runtime::ThreadPool (bounded blocking pool)
timer_wheel.h/.cpp molo::runtime::TimerWheel
signal_handler.h/.cpp molo::runtime::SignalHandler
strand.h molo::runtime::Strand (per-connection serial queue)
channel.h molo::runtime::Channel<T> (inter-loop messages)
src/molo/http/
message.h/.cpp molo::http::RequestHead, ResponseHead, Headers
connection.h/.cpp molo::http::Connection (H1/H2 dispatch, lifecycle)
listener.h/.cpp molo::http::Listener (accept, SO_REUSEPORT)
protocol_detect.h/.cpp molo::http::ProtocolDetector (ALPN, prior knowledge)
v1/parser.h/.cpp molo::http::v1::Parser (strict RFC 9112)
v1/serializer.h/.cpp molo::http::v1::Serializer
v2/connection.h/.cpp molo::http::v2::H2Connection (framing, settings)
v2/stream.h/.cpp molo::http::v2::H2Stream (state machine)
v2/hpack.h/.cpp molo::http::v2::HpackEncoder, HpackDecoder
v2/flow_control.h/.cpp molo::http::v2::FlowController
v2/settings.h molo::http::v2::Settings (caps negotiation)
src/molo/compress/
compressor.h molo::compress::ICompressor (interface)
gzip.h/.cpp molo::compress::GzipCompressor (RFC 1952)
brotli.h/.cpp molo::compress::BrotliCompressor (RFC 7932)
negotiate.h/.cpp molo::compress::Negotiator (Accept-Encoding)
src/molo/cookie/
cookie.h/.cpp molo::cookie::Cookie (parse, serialize)
jar.h/.cpp molo::cookie::CookieJar (request join, response list)
signer.h molo::cookie::ISigner (HMAC interface, impl in 03-security.md)
src/molo/static/
file_server.h/.cpp molo::static_::FileServer (conditional, ranges)
asset_provider.h molo::static_::IAssetProvider (disk + embedded)
etag.h/.cpp molo::static_::Etag (strong/weak generation)
Namespace rule: molo::runtime, molo::http, molo::http::v1, molo::http::v2, molo::compress, molo::cookie, molo::static_. No cross-namespace includes except through declared interfaces.
4. Rules (hard, numbered)
- R1. No external loop, HTTP, compression, or cookie library. Only stdlib, OS syscalls, and wolfSSL (TLS, owned by 03-security.md).
- R2. Non-blocking everywhere on loop threads. Blocking syscalls, DNS, file hashing, or compression above threshold go to ThreadPool or use async FDs.
- R3. One EventLoop per thread; connections are pinned to their loop. Cross-loop contact only via Channel. No shared mutable connection state.
- R4. Every limit in section 6 is a compile-time default plus runtime config with a static ceiling. No unbounded queue, header, body, stream, or decompressed size.
- R5. H1 parser is strict: reject ambiguous framing with 400 and close; require Host; accept chunked; answer unknown transfer coding with 501.
- R6. H2 MUST implement settings, flow control, headers/data/priority (ignored)/rst/ping/goaway/window-update; MUST NOT implement server push.
- R7. Compression never changes semantics: always send Vary: Accept-Encoding when negotiating; never compress 204/304, HEAD, or already-encoded bodies.
- R8. Static serving defaults to safe: directory indexing off, dotfiles denied, symlinks not followed unless explicitly allowed, traversal outside root impossible.
- R9. Cookies: parse per RFC 6265, join split H2 Cookie fields, never join Set-Cookie, default SameSite=Lax plus HttpOnly; Secure forced on https.
- R10. Graceful shutdown is mandatory: stop accept, drain with deadline, GOAWAY on H2, Connection: close on H1, then hard close and report.
- R11. No H2 or H1 knowledge inside handlers. Handlers see one semantic Request/Response; version differences stay inside src/molo/http.
- R12. HTTP/3 is a designed path, not v1 scope: keep a Transport interface QUIC can implement later; ship H1 + H2 only.
5. Development guidelines
- Write the interface header first, then the strictest parser or state machine, then the serializer, then integration. Each file gets a spec comment naming its RFC section.
- Prefer state machines with explicit states and transition tables for H1 parsing, H2 streams, and HPACK. Fuzz each with truncated, oversized, and interleaved inputs.
- Coroutine style:
Task<Response> handle(Request); co_await I/O, timers, pool work; honor CancellationToken; never hold a lock across co_await. - Threading: N loops equal min(CPU count, configured workers), each with own Poller; Listener uses SO_REUSEPORT where available, else single acceptor handing FDs round-robin.
- Backpressure: stop reading when the write buffer passes high watermark; resume at low watermark; H2 additionally obeys stream and connection windows.
- Errors are structured: parser errors map to 400/413/431/501 with connection action (keep or close); internal errors emit 500 via 05-request-routing.md error hierarchy.
- Observability hooks (02-observability.md) wrap accept, parse, compress, static hit/miss; disabled builds compile them out.
- Keep gzip and Brotli streaming: bounded input chunks, bounded output chunks, flush points; never buffer a full passthrough body to compress it.
6. Specifications
6.1 Event loop and threading
- Pollers: epoll (Linux), kqueue (BSD/macOS), IOCP (Windows). Default level-triggered; edge-triggered only inside Poller with drain-to-EAGAIN.
- Task: lazy start (initial_suspend_always), executor affinity to owning loop, cancellation via token checked at each await.
- ThreadPool for blocking work: default 4 threads, queue cap 1024 jobs, overflow returns 503 with Retry-After, never grows silently.
- TimerWheel: 10 ms granularity, cap 65536 timers per loop.
6.2 Connection and request lifecycle
- Stages: accept, TLS handshake (03-security.md), protocol detect, head parse, body read, dispatch, handler, serialize, keep-alive or close.
- Timeouts (defaults, all configurable down to floor, up to ceiling): header 10 s, body 30 s, idle keep-alive 75 s, handler soft budget 30 s then 503, shutdown drain 30 s.
- Caps: max connections 16384 per process (ceiling 65536); max requests per H1 connection 1000; H2 max concurrent streams 100 per connection; header block max 64 KB and 100 fields; URI max 8 KB; default body max 10 MB (routes may lower; raising above 100 MB needs explicit config and streams to disk).
- Keep-alive: H1 persistent by default;
Connection: closeor protocol error closes. H2 uses GOAWAY with last-stream-id on drain. - Signals: SIGINT/SIGTERM start drain; SIGHUP reloads config where safe; SIGTERM during drain shortens deadline to 5 s.
6.3 HTTP/1.1
- Strict start-line and field parsing per RFC 9112; obs-fold rejected; Host required; 400 on bad version, bad chunk, or Content-Length plus Transfer-Encoding conflict.
- Transfer codings: must parse chunked including extensions (ignored); unknown coding gets 501. Trailers parsed and exposed, capped by same field limits.
- Pipelining: accepted but processed sequentially in order; no parallel handler execution per connection.
- Responses: automatic Content-Length or chunked; HEAD sends head only; 204/304 never carry a body.
6.4 HTTP/2
- Entry: h2 over TLS via ALPN
h2; h2c via prior knowledge (PRI preface) or Upgrade from H1. Cleartext default in dev, TLS default in prod (TLS owned by 03-security.md). - Preface and Settings handshake mandatory; bad preface is connection error PROTOCOL_ERROR.
- HPACK: static plus dynamic table, decoder cap 4096 bytes default (SETTINGS_HEADER_TABLE_SIZE ceiling 16384); oversized header list is COMPRESSION_ERROR.
- Flow control: initial window 65535 both directions; max frame 16384 (ceiling 16777215); WINDOW_UPDATE honored; writes block on window via backpressure, never buffer unbounded.
- Streams: states idle, reserved, open, half-closed, closed per RFC 9113; max 100 concurrent; RST_STREAM on handler cancel or caps; PRIORITY frames accepted and ignored.
- No server push. No CONNECT except plain tunnels if later specified; v1 returns 405 for exotic methods unless a sibling spec claims them.
6.5 Compression
- Encodings:
gzipandbr, both in-house. Negotiation picks br over gzip when q-values tie and type is compressible. - Threshold: only bodies >= 1024 bytes and content-type in allowlist (html, json, xml, text, css, js, svg, wasm except precompressed). Never compress images, video, audio, zip, pdf, or responses with Content-Encoding already set.
- Levels: gzip 6 dynamic; brotli quality 4 dynamic, 11 for precompressed static assets at build time.
- Static precompression: build may store
.gzand.brsiblings; server selects by Accept-Encoding and sends Vary plus correct Content-Encoding and ETag variant suffix (-gz,-br). - Bomb guard: decompression of request bodies capped at 10x compressed size and 10 MB absolute; overflow is 413.
6.6 Cookies (transport only)
- Request: parse Cookie per RFC 6265; join H2 split fields with "; "; cap 50 cookies and 8 KB total; overflow is 431.
- Response: emit one Set-Cookie per cookie; attributes Domain, Path, Max-Age/Expires, Secure, HttpOnly, SameSite (Lax default, None requires Secure), Partitioned passthrough.
- Signed and encrypted values: this file defines Cookie and ISigner interfaces; crypto and key hierarchy live in 03-security.md. Format:
v1.<base64url(payload)>.<base64url(tag)>. - Size: single Set-Cookie max 4 KB; larger is a handler error, never silently truncated.
6.7 Static files
- Providers: DiskProvider (root jailed) and EmbeddedProvider (from 09-cli-ops.md embedder). Same FileServer logic over both.
- Safety: lexical normalize then verify prefix is inside root; reject NUL, backslash on Windows,
..escapes; symlinks not followed unlessfollow_symlinks=trueand target stays inside root; dotfiles denied by default. - Validators: strong ETag
"size-mtimehex"for disk,"len-hashhex"for embedded; Last-Modified from mtime; send both; honor If-None-Match then If-Modified-Since; 304 with validators and Cache-Control. - Ranges: Accept-Ranges: bytes; single range yields 206 with Content-Range; multiple ranges yield 416 in v1 (documented gap, multipart later); If-Range honored; bad range yields 416 with Content-Range: bytes */len.
- Defaults: Cache-Control
public, max-age=3600for versioned assets,no-cachefor index HTML (overridable); directory index off (403); only GET, HEAD, OPTIONS; others 405. - Performance: sendfile or TransmitFile on disk when no compression or encryption transform; otherwise bounded streaming (64 KB chunks).
6.8 HTTP/3 path (design only)
- Connection owns a
Transportabstraction (TCP today). H2 logic never touches sockets directly. A future QUIC transport can reuse semantic Request/Response, HPACK successor QPACK stays isolated in a new v3 directory. No QUIC code in v1.
7. Execution steps (in build order)
- Reactor skeleton: IPoller, EpollPoller, Task, EventLoop with timers, ThreadPool, echo TCP server, signal handling.
- Lifecycle core: Listener, Connection, timeouts, watermarks, graceful drain, structured accept/close logging hooks.
- HTTP/1.1: strict parser, serializer, keep-alive, chunked, pipelining queue, 400/413/431/501 mapping.
- H2 framing: preface, settings, HPACK codec, stream states, headers/data flow, flow control, GOAWAY/RST/PING.
- Protocol detection: ALPN wiring point, h2c prior knowledge and Upgrade, version-independent dispatch.
- Compression: gzip encoder/decoder, brotli encoder/decoder, negotiator, thresholds, Vary, bomb guards.
- Cookies: parser, serializer, jar join/split, signer interface with stub, attribute defaults.
- Static: providers, traversal guard, ETag/Last-Modified, conditional, single range, cache headers, sendfile path.
- Hardening pass: all caps enforced, fuzz harnesses for H1/H2/HPACK/cookies, slowloris and large-body tests.
- Integration seam: hand semantic Request/Response to 05-request-routing.md, export counters to 02-observability.md, accept TLS handle from 03-security.md.
8. Validation (how to verify each step)
- Step 1: echo server holds 10k idle connections under 512 MB; kill -INT drains in under 30 s; blocking job overflow returns 503 not hang.
- Step 2: header/body/idle timeouts fire within 10 percent; high/low watermarks pause and resume reads; drain closes listeners first then connections.
- Step 3: RFC 9112 vectors pass (chunked, trailers, bad framing gives 400+close, unknown TE gives 501); keep-alive reuses connection for 1000 requests; pipelined requests answer in order.
- Step 4: h2spec-style checks pass for settings, window updates, stream reset, GOAWAY; HPACK round-trips interop vectors; oversized header list triggers COMPRESSION_ERROR.
- Step 5: ALPN h2 negotiates H2, http/1.1 negotiates H1; prior-knowledge h2c works; H1 Upgrade to h2c works; wrong preface closes with PROTOCOL_ERROR.
- Step 6: Accept-Encoding br/gzip/identity matrix returns right encoding plus Vary; bodies under 1024 bytes stay identity; bomb input stops at cap with 413.
- Step 7: RFC 6265 cookie vectors parse; H2 split Cookie joins correctly; Set-Cookie never merged; oversized jar returns 431.
- Step 8: traversal attempts (
.., encoded, NUL, symlink escape) all fail closed; conditional requests return 304; single range returns 206 and bad range 416; embedded and disk providers return byte-identical bodies. - Step 9: fuzzers run 1M mutated inputs with zero crashes or hangs; slowloris with 1000 trickling connections stays under caps.
- Step 10: integration suite from 10-workflow-example.md passes end to end through real sockets for H1 and H2; disabled observability changes throughput by under 2 percent.
9. References
- Depends on: 02-observability.md (event, metric, trace hooks and zero-cost macros); 03-security.md (TLS handle, cookie signing and encryption, body and header ceilings alignment).
- Feeds: 05-request-routing.md (semantic Request/Response, app and router dispatch); 06-realtime.md (H1 Upgrade path for WebSocket, H2 streams for SSE); 08-clients-openapi.md (framing and HPACK reuse for test and REST clients); 09-cli-ops.md (embedded asset format, dev server listener reuse, health endpoints).
- Parallel: 04-data.md and 07-supervisor-replication.md consume lifecycle and Task but define their own pooling and durability.
10. Open questions
- Default loop count: thread-per-core with SO_REUSEPORT, or single acceptor plus worker loops for simpler Windows parity?
- H2 PRIORITY: fully ignore, or keep a minimal scheduling hint for SSE vs API multiplexing?
- Multipart ranges in v1 or documented 416 gap until demand appears?
- Brotli static dictionary: ship full default dictionary or trimmed web subset for binary size?
- Cookie chunking for >4 KB signed values: reject, or support
name_0..Nreassembly?
11. Sources
- RFC 9112 HTTP/1.1, R. Fielding et al., Internet Standard, https://www.rfc-editor.org/rfc/rfc9112 (retrieved 2026-10-23).
- RFC 9113 HTTP/2, M. Thomson et al., Proposed Standard obsoleting RFC 7540/8740, https://www.rfc-editor.org/rfc/rfc9113 (retrieved 2026-10-23).
- RFC 9110 HTTP Semantics, R. Fielding et al., Internet Standard, https://www.rfc-editor.org/rfc/rfc9110 (retrieved 2026-10-23).
- RFC 6265 HTTP State Management Mechanism, A. Barth, https://www.rfc-editor.org/rfc/rfc6265 (retrieved 2026-10-23).
- RFC 7932 Brotli Compressed Data Format, https://www.rfc-editor.org/rfc/rfc7932 (retrieved 2026-10-23); RFC 1952 GZIP File Format 4.3, https://www.rfc-editor.org/rfc/rfc1952 (retrieved 2026-10-23).
- C++ coroutines reference, https://en.cppreference.com/cpp/language/coroutines (retrieved 2026-10-23).
- epoll man page man7/epoll.7, https://man7.org/linux/man-pages/man7/epoll.7.html (retrieved 2026-10-23); Windows async I/O and IOCP, https://learn.microsoft.com/en-us/windows/win32/fileio/synchronous-and-asynchronous-i-o (retrieved 2026-10-23).