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.
212 lines
21 KiB
Markdown
212 lines
21 KiB
Markdown
# 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)
|
|
|
|
1. 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).
|
|
2. 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.
|
|
3. 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.
|
|
4. 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).
|
|
5. 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.
|
|
6. 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).
|
|
7. 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.
|
|
8. 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.
|
|
9. 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.
|
|
10. 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<T>; 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<T>. 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<T>: 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: close` or 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: `gzip` and `br`, 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 `.gz` and `.br` siblings; 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 unless `follow_symlinks=true` and 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=3600` for versioned assets, `no-cache` for 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 `Transport` abstraction (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)
|
|
|
|
1. Reactor skeleton: IPoller, EpollPoller, Task<T>, EventLoop with timers, ThreadPool, echo TCP server, signal handling.
|
|
2. Lifecycle core: Listener, Connection, timeouts, watermarks, graceful drain, structured accept/close logging hooks.
|
|
3. HTTP/1.1: strict parser, serializer, keep-alive, chunked, pipelining queue, 400/413/431/501 mapping.
|
|
4. H2 framing: preface, settings, HPACK codec, stream states, headers/data flow, flow control, GOAWAY/RST/PING.
|
|
5. Protocol detection: ALPN wiring point, h2c prior knowledge and Upgrade, version-independent dispatch.
|
|
6. Compression: gzip encoder/decoder, brotli encoder/decoder, negotiator, thresholds, Vary, bomb guards.
|
|
7. Cookies: parser, serializer, jar join/split, signer interface with stub, attribute defaults.
|
|
8. Static: providers, traversal guard, ETag/Last-Modified, conditional, single range, cache headers, sendfile path.
|
|
9. Hardening pass: all caps enforced, fuzz harnesses for H1/H2/HPACK/cookies, slowloris and large-body tests.
|
|
10. 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<T> but define their own pooling and durability.
|
|
|
|
## 10. Open questions
|
|
|
|
1. Default loop count: thread-per-core with SO_REUSEPORT, or single acceptor plus worker loops for simpler Windows parity?
|
|
2. H2 PRIORITY: fully ignore, or keep a minimal scheduling hint for SSE vs API multiplexing?
|
|
3. Multipart ranges in v1 or documented 416 gap until demand appears?
|
|
4. Brotli static dictionary: ship full default dictionary or trimmed web subset for binary size?
|
|
5. Cookie chunking for >4 KB signed values: reject, or support `name_0..N` reassembly?
|
|
|
|
## 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).
|