Under construction

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.

1 binary + 19 libraries Rust 1.99 · edition 2024 · v0.0.5 Acyclic; hj-config at the root ≈2,000 test functions Vendored forks: monoio, monoio-rustls (real writev), dashmap, pprof

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-configXML tree → model · parse_bundle hj-corethe frozen contract · ReqCtx · Router · budgets handlers & transports (depend on hj-core) hj-tlshj-h2hj-lsapihj-fastcgihj-statichj-compresshj-proxy hj-aclhj-extensionhj-http (ServeConfig)hj-pagecache (uses hj-cache) hj-cachesharded byte-LRU hj-acme · hj-ocsp--features acme / ocsp isolated (no hj-* deps)hj-rewritehj-loghj-geohj-cache httpjeturing transport · pipeline · lscache glue · adminconfig_transaction · resource_generation · waf · otel
hj-config → hj-core → handlers/transports → the httpjet binary. Isolated crates converge at the binary; hj-geo feeds hj-acl's GeoRules; the two feature-gated crates hang off hj-tls's resolver handles.

Crate map

CrateRole
hj-coreProtocol-agnostic Request/Response/Body, ReqCtx, the vhost Router, and shared budgets (BodyBufferBudget leases, BandwidthThrottle, ResponseCompletion) — the seam every handler programs against.
hj-configLiteSpeed 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-tlsOne 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 featureRFC 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 featureBounded OCSP fetch + OpenSSL 3.x signature/issuer/freshness validation; status lifecycle with revocation enforcement.
hj-httpThe 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-h2The 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-lsapiNative 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-fastcgiBounded FastCGI record codec, connection pool (poison-by-default), strict CGI response parser and streaming handler for explicit fcgi/fastcgi processors.
hj-staticStatic files: openat2(RESOLVE_BENEATH) resolution, fileETag ETags, conditional requests, single + multipart ranges, precompressed .br/.gz siblings, Body::File with pinned inodes for sendfile.
hj-cacheThe shared ShardedCache byte-weighted LRU primitive and CacheCaps sizing (the former mmap file cache was removed — it had no consumers).
hj-pagecacheThe 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-compressgzip/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-rewriteApache 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-proxyReverse proxy (h1, h2://, h2s://), upstream groups with weighted selection and active health checks, passive circuit breaker, WebSocket relay, SSE streaming, upstream mTLS.
hj-aclAllow/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-geoOffline GeoIP/ASN sources: the CidrList text format and binary-search interval sets. No runtime fetches.
hj-logNon-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-extensionExtensionRegistry for compile-time pre-handlers and response transforms (the shipped registry is empty).
httpjetThe 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).

one monoio io_uring runtime per listener fd, pinned to its corelisteners: systemd socket activation or self-bound, SO_REUSEPORT ×N workersplainsecurehttp/1.xh2chttp/1.1h2miss or dynamicresponse: buffered ≤ 256KiB, streamed above

TCP :80

TCP :443

UDP :443

unix:/path

PROXY protocol strip (per-listener opt-in)

monoio-rustls handshake · ALPN · optional kTLS

cleartext sniff: 24-byte h2 preface?

H1 codec (httparse, sendfile)

hj-h2 · one task per connection

quinn-proto QUIC driver · HTTP/3 · QPACK

on-core fast path: finished-response memo · static · cache hit

uring::bridge — hand the request to the tokio runtime

pipeline::handle() · ServerState · LSAPI pool · page cache · reload · metrics

One transport, three protocols, one pipeline. Cheap work stays on the monoio core: the H1 codec, the native h2 task, the QUIC driver, and the on-core fast path (finished-response memo, static files, page-cache hits). Anything else crosses 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" --> bridge

Ring 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:

hj-core · the seam
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.

i
Extending without touching handle()

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.

!
Load-bearing detail

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

PathContents
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}.rsThe 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}.rsFeature 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.

SIGHUP · PUT /v1/configparse error ·restart_required · stalerevision · missing vhostfileexact revision recheckpassesserving generationuntoucheda newer generationpublishesin-flight requests finishon their pinnedgeneration

Candidate

Rejected

Published

Serving

Retiring

caches keyed by epoch + generation:
old renders cannot overwrite new entries

Candidate → published → retired. A rejected candidate never touches the serving generation. Trust changes (TLS material, mTLS policy, PROXY protocol) retire the old listener group and drain its connections under their pinned policy; boot-owned resources (listener topology, PHP pools, cache stores, privileges) are 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 note

The full ownership inventory and the transactional replacement sequence are on Admin API & reload.

Code conventions

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.