This documentation is still in progress. Some information may be incomplete or out of date. Please check back later.
Docs/Extending httpjet
Running it
Extending httpjet
Two extension points, each with a deliberately narrow contract: compile-time extensions — trusted Rust crates linked into a custom build that register read-only pre-handlers and response transforms — and the WAF sidecar, a bounded loopback HTTP inspection call for an independently deployed engine. There is no dynamic plugin ABI and no in-process sandbox, on purpose.
Compile-time extensions
hj-extension::ExtensionRegistry is assembled at the binary's registration point, crates/httpjet/src/extensions.rs. The distributed binary registers nothing, so the facility is inert unless an operator maintains a custom build. It is a source-level compatibility contract over the frozen hj-core types, not a stable native ABI.
pub(crate) fn compiled_registry() -> hj_extension::ExtensionRegistry {
let mut registry = hj_extension::ExtensionRegistry::new();
registry.register_pre_handler("company.request-policy", company_policy::hook()).expect("unique");
registry.register_response_transform("company.headers", company_headers::transform()).expect("unique");
registry
}Registration order is execution order. Names are globally unique, 1–64 bytes of lowercase ASCII letters, digits, ., - and _. Registered extensions are logged once at boot; changing them means rebuilding and restarting, and SIGHUP carries the same linked registry into the next generation. Extension settings belong to the extension crate — the registry does not reinterpret LiteSpeed XML and exposes no runtime loader.
diagram source (mermaid)
flowchart TB
classDef ext fill:#2a2314,stroke:#fbbf73,color:#fff
classDef host fill:#13203a,stroke:#818cf8,color:#fff
A["parse · vhost · mTLS gate · client IP · ACL · geo · throttle"] --> P["extension PreHandlers<br/>(read-only RequestView; Continue · Respond · Err)"]:::ext
P --> B[".htaccess · rewrite · cache lookup · dispatch"]:::host
B --> S["page-cache store (inside dispatch)"]:::host
S --> T["extension ResponseTransforms<br/>(egress only; re-applied on cache hits)"]:::ext
T --> H["host transforms: promote · expires · sub_filter · compress · deny-CDN-cache · Alt-Svc"]:::host
P -. "any registered pre-handler disables the on-core fast path" .-> BPre-handler contract
A PreHandler receives an immutable ReqCtx and a RequestView exposing only method, URI and headers — no body, no mutable parts. It returns Continue (next hook, then built-in dispatch), Respond(response) (skip remaining hooks and dispatch; normal transforms, metrics, request-id handling and access logging still run), or Err(HandlerError) (fail closed to that status and log the named failure). Pre-handlers never see reserved operational routes intercepted before vhost dispatch (ACME challenges, loopback cache endpoints) or requests rejected before the hook boundary. Any registered pre-handler disables the on-core memo/cache/static fast path, because that synchronous path cannot run async hooks; response-only extensions keep it.
Response-transform contract
Registered ResponseTransforms run in order on every full-pipeline response and every on-core cache/static response, before the host's static promotion, expires, sub_filter, compression, redirect-cache guard and Alt-Svc transforms — so an extension sees the origin representation before compression and cannot bypass the final transport-header policy by ordering. There is no error return: keep work bounded and express fallible initialization at registration.
Extension code runs with httpjet's memory and filesystem authority. A panic, deadlock, unbounded allocation or blocking call affects the server. Only trusted, reviewed crates belong in the registry.
WAF sidecar
httpjet embeds no rules engine and the ignored LiteSpeed lsrecaptcha/censorshipControl elements stay inactive. Instead, --waf-sidecar-addr turns on a bounded inspection call to a loopback HTTP service — the integration point for an existing maintained engine such as a Coraza/OWASP CRS adapter. httpjet does not claim CRS compatibility; the adapter owns the translation from the stable inspection document to its engine's model.
--waf-sidecar-addr 127.0.0.1:19090 # required loopback address; absent = seam completely inert
--waf-sidecar-path /inspect # absolute, no control characters
--waf-timeout-ms 100 # total deadline: admission + allowance + connect + write + parse (≤ 30 s)
--waf-body-max 65536 # bodies at or below this are sent as body_hex (≤ 1 MiB)
--waf-concurrency 128 # ≤ 4096
--waf-fail-open # weaker policy: continue on sidecar failure (every bypass is logged)diagram source (mermaid)
sequenceDiagram
participant C as client
participant J as httpjet pipeline
participant W as WAF sidecar<br/>(loopback, e.g. Coraza adapter)
C->>J: request
J->>J: decode/normalize URI · enforce body limit · resolve client IP
J->>J: reserve serialization allowance (64 MiB process budget) within --waf-timeout-ms
J->>W: POST /inspect {"version":1, method, path, query, vhost, client_ip, protocol, tls, headers_hex, body_hex ≤ --waf-body-max | body_omitted:true}
alt 204 + Content-Length: 0
W-->>J: allow
J->>J: continue: rewrite → cache → dispatch
else 403 + Content-Length: 0
W-->>J: block
J-->>C: 403
else timeout · EOF · other status · malformed head (> 4 KiB)
W--xJ: sidecar failure
J-->>C: 503 (fail-closed) — or continue if --waf-fail-open (logged)
end
Note over J,W: one close-delimited HTTP/1.1 request per inspection — connections never reused- Document.
version: 1, method, normalized path, query, resolved vhost, resolved client IP, protocol, effective TLS state, all request headers. Header values and the body are lowercase hexadecimal so raw bytes are lossless and cannot inject log or protocol controls; adapters must reject unsupported versions. - Bodies. ≤
--waf-body-max→body_hex, restored byte-identically for downstream processing; larger →body_omitted: trueand the sidecar must decide that shape explicitly. The global request limit stays authoritative. - Memory. Beyond the concurrency cap, all generations share a 64 MiB serialization budget; each inspection reserves a byte-weighted allowance before creating hex/JSON copies, so stalled inspections cannot multiply temporary heap up to the concurrency limit. Waiting for the allowance is inside the deadline and follows the fail policy.
- Transport. One close-delimited HTTP/1.1 POST per inspection, never reused (no response-boundary ambiguity); response head ≤ 4 KiB; duplicate/nonzero Content-Length, Transfer-Encoding, malformed status, EOF and any status other than 204/403 are failures.
cargo test -p httpjet --bin httpjet pipeline::e2e::waf_runs_before_static_cache_and_can_block_a_previously_allowed_path
cargo test -p httpjet --bin httpjet pipeline::e2e::waf_sidecar_failure_policy_is_closed_unless_explicitly_opened
cargo test -p hj-extensionWhy there is no dynamic or sandboxed plugin ABI
httpjet does not dlopen third-party trait objects: Rust promises no stable trait-object ABI across independently built shared libraries, and a native module would share allocator, signal, thread and process authority with the server. There is no current requirement to execute untrusted third-party code, so a WASM runtime and its dependency and attack surface are not included. Untrusted or independently deployed inspection belongs behind the bounded sidecar contract above. If an untrusted-plugin workload appears, it must first define capabilities, memory and fuel limits, deadline and cancellation behavior, body access, state and network permissions, version negotiation, and fail-open/closed semantics — only then would an isolation mechanism be selected.
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.