This documentation is still in progress. Some information may be incomplete or out of date. Please check back later.
Docs/Architecture
Guide
Architecture
httpjet is a Cargo workspace: one binary crate (httpjet) over nineteen hj-* library crates. The graph is acyclic, hj-config is the root, and hj-core is the small frozen seam every handler programs against. Two crates (hj-acme, hj-ocsp) only link in behind build features. Roughly 2,000 test functions span the workspace (2,028 green at v0.0.5).
Workspace shape
Responsibilities are split so the contract a handler depends on stays tiny while transports, protocols, gateways and the cache evolve behind it. Four libraries are deliberately isolated — hj-rewrite, hj-log, hj-geo and hj-cache take no hj-* dependency, so their logic is portable and testable on its own. hj-pagecache is transport-agnostic and builds on hj-cache's sharded LRU; its request-aware glue lives in the binary.
Crate dependency graph
hj-config parses the LiteSpeed XML tree into one normalized model; hj-core re-exports it so every downstream crate imports the configuration model through core — one import surface, never inverted. The binary is the only crate that sees everything.
hj-geo feeds hj-acl's GeoRules; the two feature-gated crates hang off hj-tls's resolver handles.Crate map
| Crate | Role |
|---|---|
| hj-core | Protocol-agnostic Request/Response/Body, ReqCtx, the vhost Router, and shared budgets (BodyBufferBudget leases, BandwidthThrottle, ResponseCompletion) — the seam every handler programs against. |
| hj-config | LiteSpeed XML → one normalized model ($VAR substitution, OLS-faithful value semantics, fail-closed defaults). parse_bundle is the in-memory variant used by the transactional API. |
| hj-tls | One rustls ServerConfig for TCP + QUIC: SNI resolver (no cert-name-match, like OLS), dual ECDSA/RSA chains, mTLS verifier with CRLs, generation-bound staple slots, certificate reload handles. |
| hj-acme feature | RFC 8555 HTTP-01/DNS-01 issuance and renewal (instant-acme), the 0700 PrivateStore with atomic snapshots, challenge registry, DNS webhook adapter with durable cleanup journal. |
| hj-ocsp feature | Bounded OCSP fetch + OpenSSL 3.x signature/issuer/freshness validation; status lifecycle with revocation enforcement. |
| hj-http | The shared ServeConfig connection-tuning struct derived from <tuning>. Its old hyper-H1/quinn-H3 adapters were removed when io_uring became the only transport. |
| hj-h2 | The native HTTP/2 server: one task per connection, send flow control, streaming bodies, one vectored write per wake, RFC 9113 validation, and the zero-allocation HPACK codec (hj_h2::hpack). |
| hj-lsapi | Native LSAPI wire codec (vendored lsapidef.h constants), UDS client pool with generation epochs, the lsphp supervisor and subreaper, the CGI env builder, and the fail-closed jail. |
| hj-fastcgi | Bounded FastCGI record codec, connection pool (poison-by-default), strict CGI response parser and streaming handler for explicit fcgi/fastcgi processors. |
| hj-static | Static files: openat2(RESOLVE_BENEATH) resolution, fileETag ETags, conditional requests, single + multipart ranges, precompressed .br/.gz siblings, Body::File with pinned inodes for sendfile. |
| hj-cache | The shared ShardedCache byte-weighted LRU primitive and CacheCaps sizing (the former mmap file cache was removed — it had no consumers). |
| hj-pagecache | The origin page store: sharded byte-LRU index with W-TinyLFU admission, keys + vary + tags + TTL/stale windows, the LSCache header classifier, the HJPC file tier with boot scan, dictionary recompression, single teardown path. |
| hj-compress | gzip/brotli/zstd response encoders, server-priority negotiation, streaming CompressStream, per-thread zstd contexts, and the per-vhost PageDictRegistry the page cache uses for internal dedup. |
| hj-rewrite | Apache mod_rewrite / .htaccess: dense-DFA prefilter, linear-first regex with fancy-regex fallback, access control, scoped Basic auth, header ops, LSCache directives, memo-eligibility classification. |
| hj-proxy | Reverse proxy (h1, h2://, h2s://), upstream groups with weighted selection and active health checks, passive circuit breaker, WebSocket relay, SSE streaming, upstream mTLS. |
| hj-acl | Allow/deny ACLs with trusted-proxy (T) marking, real-IP resolution and anti-spoofing, the sharded per-IP throttle, and GeoRules (geo/ASN resolved to CIDR sets at state build). |
| hj-geo | Offline GeoIP/ASN sources: the CidrList text format and binary-search interval sets. No runtime fetches. |
| hj-log | Non-blocking access + error logging: common/combined/JSON access lines, text or JSON error events with redaction, size rolling with day pruning, the syslog tap. |
| hj-extension | ExtensionRegistry for compile-time pre-handlers and response transforms (the shipped registry is empty). |
| httpjet | The binary: listener planning and adoption, the uring/ transport, pipeline/, lscache/ glue, the fast-path memo, metrics, admin listeners, config transactions and resource generations, WAF client, OTel, PHP slow log, memory trim. |
Transport paths
The pure io_uring transport is the only serving transport. There is no runtime switch and no URING=0 rollback build; reverting is a rebuild from an older revision. One monoio runtime runs per inherited or self-bound SO_REUSEPORT listener fd, pinned to its core when the worker count equals the CPU count (--no-core-pinning is the kill switch).
uring::bridge into the tokio runtime that owns ServerState, the LSAPI pool, the page cache and reload state, then returns to the monoio core for framing and writes. Bodies ≤ 256 KiB are buffered; larger ones stream on H1, H2 and H3 alike.diagram source (mermaid)
flowchart TB
classDef core fill:#13203a,stroke:#818cf8,color:#fff
classDef k fill:#0a0d17,stroke:#2c3856,color:#c2cadd
subgraph sockets ["listeners: systemd socket activation or self-bound, SO_REUSEPORT ×N workers"]
direction LR
tcp80["TCP :80"]:::k
tcp443["TCP :443"]:::k
udp443["UDP :443"]:::k
uds["unix:/path"]:::k
end
subgraph cores ["one monoio io_uring runtime per listener fd, pinned to its core"]
direction TB
pp["PROXY protocol strip (per-listener opt-in)"]
tls["monoio-rustls handshake · ALPN · optional kTLS"]
sniff["cleartext sniff: 24-byte h2 preface?"]
h1["H1 codec (httparse, sendfile)"]
h2["hj-h2 · one task per connection"]
quic["quinn-proto QUIC driver · HTTP/3 · QPACK"]
memo["on-core fast path: finished-response memo · static · cache hit"]
end
tcp80 --> pp
tcp443 --> pp
uds --> sniff
udp443 --> quic
pp -- plain --> sniff
pp -- secure --> tls
sniff -- "http/1.x" --> h1
sniff -- "h2c" --> h2
tls -- "http/1.1" --> h1
tls -- "h2" --> h2
h1 --> memo
h2 --> memo
quic --> memo
memo -- "miss or dynamic" --> bridge["uring::bridge — hand the request to the tokio runtime"]:::core
bridge --> pipe["pipeline::handle() · ServerState · LSAPI pool · page cache · reload · metrics"]:::core
pipe -- "response: buffered ≤ 256 KiB, streamed above" --> bridgeRing setup is tunable but deliberately plain: --uring-ring legacy (default) uses flagless rings because the measured coop and defer variants were noise or a ~2% loss on the virtualized production guest; ring size stays at the default because ring memory is memlock-charged. Multishot accept is on by default (--no-multishot-accept reverts to one SQE per connection).
The frozen contract — hj-core
This is the seam that lets handlers compose without knowing the transport. Changing these signatures means updating every dependent, so they are treated as frozen:
enum Body { Empty, Full(Bytes), Stream(BoxBody), File(FileBody) }
type Request = http::Request<IncomingBody>;
type Response = http::Response<Body>;
// every terminal handler (StaticFiles, Lsapi, FastCgi, ProxyHandler)
trait Handler { async fn handle(&self, &mut ReqCtx, Request) -> Result<Response, HandlerError>; }
// every post-handler munger (promote → expires → sub_filter → compress → deny-CDN-cache → Alt-Svc)
trait ResponseTransform { async fn transform(&self, &ReqCtx, &mut Response); }
FileBody may carry an open descriptor that pins the selected inode across a concurrent cache version swap, recompression or unlink — that is what lets sendfile and the file tier coexist safely. ReqCtx carries the per-request state: server/vhost (Arcs of the pinned generation), peer_ip/client_ip (post-forwarding, or peer_unix), is_tls, protocol, trusted_proxy, and an env vec that holds rewrite [E=] sets and CGI overrides. Router::resolve(listener, key) maps SNI/Host to a vhost.
New terminal handlers impl Handler and register in pipeline/mod.rs::dispatch(); new response munging impl ResponseTransform and is pushed into state.rs::build_transforms. Out-of-tree code uses the same traits through the compile-time extension registry.
The request pipeline
pipeline::handle() is the integration point: vhost resolution → mTLS trust boundary → client-IP/ACL/geo/throttle → optional WAF inspection → extension pre-handlers → dispatch() → response transforms → access log, metrics and spans. dispatch() mirrors OpenLiteSpeed's ordering with the page-cache seams inserted once rewrite and access state are known:
The terminals are intentionally not a uniform matcher registry. Their predicates differ (request headers for WebSocket, URI prefix for proxy contexts, script-path split for suffix routing), their fall-through rules differ, and their post-processing differs, so an explicit if/return chain keeps the security-relevant ordering legible. Two fall-throughs matter: a proxy context that cannot resolve its ext-processor falls through to static, while a script whose handler has no pool hard-fails 503 and must never fall through — falling through would serve the raw PHP source.
A rewrite to index.php keeps the original REQUEST_URI: the resolved script is pinned separately via the LsapiScript request extension, with SCRIPT_NAME/QUERY_STRING pushed into ctx.env. That is what makes XenForo-style front-controller routing correct — and why the page cache must key by the original URI, never the rewrite-resolved path.
The cache seams are inert unless serve runs with --page-cache: lscache::cache_lookup runs after rewrite/ACL but before any backend (a hit short-circuits dispatch), and lscache::cache_store runs after finalize_response at each terminal return. Both key off a single CacheCtx built once per dispatch.
Module organization
| Path | Contents |
|---|---|
crates/httpjet/src/uring/ | The transport: mod.rs (accept loops, sniff, TLS), codec.rs (H1), h3.rs (QUIC driver), bridge.rs, ktls.rs + ktls_policy.rs, proxy_protocol.rs, request_body.rs (decompression), worker_group.rs (listener generations). |
crates/httpjet/src/pipeline/ | mod.rs (handle/dispatch/finalize_response), rewrite_glue, proxy_glue (incl. ProxyHandler), htaccess_apply, suffix_routing, scoped_auth, fast_memo, response_util, and the in-file e2e/security regression tests. |
crates/httpjet/src/lscache/ | mod.rs (CacheCtx, cache_lookup/cache_store), hit.rs (hit-response construction + precompress), singleflight.rs. |
crates/httpjet/src/{admin*,config_transaction,resource_generation,serving_generation,listener_plan,tcp_candidate}.rs | The read-only status API, the authenticated write API, the publication coordinator, and the listener/resource generation model behind SIGHUP and transactional reloads. |
crates/httpjet/src/{acme_runtime,ocsp_runtime,otel,waf,extensions,metrics,telemetry,phpslow,memtrim}.rs | Feature runtimes and observability. |
crates/hj-h2/src/server/ | state/recv/send + serve(); HPACK is the hpack submodule. |
crates/hj-rewrite/src/ | rules/{parse,eval}, htaccess/{parse,mod_access,auth_scope,cache,php,scope_index}, auth.rs (htpasswd). |
crates/hj-lsapi/src/ | proto/{frame,builder}, pool.rs, supervisor, and the security-critical jail. |
Configuration generations
Live state is published as immutable generations behind an ArcSwap. A SIGHUP or an API transaction builds a complete candidate (config, rewrite caches, health schedulers) with background work inactive, validates it, then publishes it atomically under one coordinator; requests already in flight finish on the generation they pinned at acceptance.
restart_required.diagram source (mermaid)
stateDiagram-v2
direction LR
[*] --> Candidate: SIGHUP · PUT /v1/config
Candidate --> Rejected: parse error · restart_required · stale revision · missing vhost file
Candidate --> Published: exact revision recheck passes
Rejected --> [*]: serving generation untouched
Published --> Serving
Serving --> Retiring: a newer generation publishes
Retiring --> [*]: in-flight requests finish on their pinned generation
note right of Published
caches keyed by epoch + generation:
old renders cannot overwrite new entries
end noteThe full ownership inventory and the transactional replacement sequence are on Admin API & reload.
Code conventions
- Formatting: rustfmt-clean (default settings, edition 2024);
cargo fmt --all --checkis a CI gate. - Errors:
thiserrortypes at system boundaries; internal invariant violations panic with a message rather than pretending to recover. - unsafe: rare, and never in HPACK. The real sites are
hj-h2's pin projection and writev/iovec plumbing, the validated fixed-size mmaps inhj-pagecache/diskstore.rsandhj-lsapi/pool.rs, and small libc shims — each with its safety invariant documented above the block. - Async: tokio tasks for independent work,
FuturesUnorderedfor multiplexing with coordinated shutdown, no per-stream spawn on hot paths. - Hashing:
rustc-hashonly for internal maps keyed by trusted values (h2 stream ids); anything keyed by untrusted input keeps SipHash. - Allocator: mimalloc globally, with a background
mi_collecttrim that only runs above an RSS threshold (--mimalloc-trim-*).
httpjet · documentation · synced to the source tree on 2026-10-13 (v0.0.5).
Source: github.com/faratech/httpjet · Home: httpjet.net · Found an error? Open an issue on the repository.