Under construction

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.

extensions.rs · registration
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.

any registeredpre-handler disables theon-core fast path

parse · vhost · mTLS gate · client IP · ACL · geo · throttle

extension PreHandlers
(read-only RequestView; Continue · Respond · Err)

.htaccess · rewrite · cache lookup · dispatch

page-cache store (inside dispatch)

extension ResponseTransforms
(egress only; re-applied on cache hits)

host transforms: promote · expires · sub_filter · compress · deny-CDN-cache · Alt-Svc

Where hooks run. Pre-handlers run after trust and access resolution but before everything that could serve a byte; response transforms run after dispatch (and after page-cache storage) but before httpjet's own transforms, so an extension changes egress, not the canonical cached object, and is re-applied on hits.
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" .-> B

Pre-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.

!
In-process means full authority

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.

serve · sidecar flags
--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)
WAF sidecar(loopback, e.g. Coraza adapter)httpjet pipelineclientalt[204 + Content-Length: 0][403 + Content-Length: 0][timeout · EOF · other status · malformed head (> 4 KiB)]one close-delimited HTTP/1.1 request per inspection — connections never reusedrequestdecode/normalize URI · enforce body limit · resolve client IPreserve serialization allowance (64 MiB process budget) within --waf-timeout-msPOST /inspect {"version":1, method, path, query, vhost, client_ip, protocol, tls, headers_hex, body_hex ≤ --waf-body-max | body_omitted:true}allowcontinue: rewrite → cache → dispatchblock403sidecar failure503 (fail-closed) — or continue if --waf-fail-open (logged)
One inspection per request. Inspection runs after URI decoding/normalization and body-limit enforcement but before rewrite, cache lookup and any backend — background cache refreshes go through the same dispatch and are inspected too. Only two replies are valid; everything else is a sidecar failure.
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
tests
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-extension

Why 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.