This documentation is still in progress. Some information may be incomplete or out of date. Please check back later.
Docs/Proxy & gateways
Internals
Proxy & gateways
Everything that hands a request to another process: the reverse proxy (HTTP/1, h2://, h2s://) with weighted upstream groups and active health checks, WebSocket and SSE relay, and the opt-in FastCGI gateway. LSAPI/PHP has its own page. Generic CGI execution and uWSGI are specified but deliberately not routable.
How a request reaches a backend
Three routes select a gateway, in dispatch order: a WebSocket map (<websocketList>) when the request carries an upgrade, a proxy <context> whose URI prefix matches and whose handler names a proxy extProcessor, and a rewrite [P] target (deferred so it can still participate in the page cache). Script suffixes (scriptHandler) route to LSAPI or FastCGI. A proxy context that cannot resolve its processor falls through to static; a script handler that cannot resolve its pool hard-fails 503.
| Target form | Protocol to the backend |
|---|---|
host:port, ip:port, uds:///path, http://… | HTTP/1.1 with keep-alive pools |
https://… | HTTP/1.1 over TLS; backend certificate verified; optional client cert (clientCertFile/clientKeyFile) |
h2://host:port httpjet | HTTP/2 prior knowledge (cleartext); multiplexes requests on one backend connection |
h2s://host[:443] httpjet | HTTP/2 over TLS, ALPN fail-closed |
Pool keys are scheme-scoped, so an h1 and an h2 target at the same address never share a connection. Forwarded requests to h2 upstreams are re-absolutized (the h2 client derives :scheme/:authority from the URI). Proxied responses may not assert X-LiteSpeed-Purge/-Cache-Control/-Vary — those are stripped so a remote backend cannot inject cache policy. Named upstreams receive a W3C traceparent when tracing is on.
Since 2026-09-16 a rewrite [P] URL that resolves (after DNS — /etc/hosts pins cannot bypass the screen) to loopback, link-local or a Unix socket is refused unless it matches an operator-configured extProcessor address. This closes the class where a .htaccess author reaches the local trust plane (metrics, admin, purge endpoints) through the server.
Upstream groups & selection
An extProcessor with several <address> children is a group. Without the options below it keeps ordered primary/failover behavior with a passive circuit breaker (3 consecutive dial failures mark a peer bad; one successful dial clears it — nginx max_fails semantics). With them it becomes a load-balanced group:
<extProcessor>
<name>application</name>
<type>proxy</type>
<address>127.0.0.1:9001</address>
<address>127.0.0.1:9002</address>
<loadBalancePolicy>weighted-round-robin</loadBalancePolicy>
<!-- primary-first (default) | weighted-round-robin | weighted-least-active -->
<addressWeights>2,1</addressWeights> <!-- one integer 1..1000 per address -->
<healthCheck>
<mode>GET</mode> <!-- connect | GET | HEAD (default connect) -->
<path>/ready</path> <host>application.internal</host>
<interval>10</interval> <timeout>2</timeout> <!-- 1..3600 s; timeout ≤ interval -->
<rise>2</rise> <fall>3</fall> <!-- 1..100 -->
<expectedStatus>200</expectedStatus> <!-- 200..599 -->
</healthCheck>
</extProcessor>diagram source (mermaid)
flowchart TB
classDef pol fill:#13203a,stroke:#818cf8,color:#fff
R["request → proxy context or [P] rewrite"] --> G["resolve extProcessor group<br/>(server or vhost scope)"]
G --> E["eligible peers = active-health OK ∧ breaker closed"]
E --> P("loadBalancePolicy"):::pol
P -- "primary-first (default)" --> S1["first eligible in address order"]
P -- weighted-round-robin --> S2["smooth weighted RR<br/>addressWeights 1..1000"]
P -- weighted-least-active --> S3["min(active ÷ weight), smooth-WRR tie-break"]
S1 & S2 & S3 --> C["checkout pooled connection<br/>h1 · h2:// · h2s:// (scheme-scoped pool keys)"]
C -- "dial fails" --> F["mark bad, try next eligible<br/>(no unsafe-body replay)"]
F --> E
C -- "response head" --> H["stream body / SSE / WebSocket relay<br/>reservation released on completion"]
E -- "none eligible" --> X[503]rise consecutive successes; fall failures take it down; probe success restores it without user traffic. Passive breaker state is independent. All-ineligible → 503.diagram source (mermaid)
stateDiagram-v2
direction LR
[*] --> Cold: healthCheck configured
[*] --> Eligible: no healthCheck (passive only)
Cold --> Eligible: rise consecutive probe successes
Eligible --> Down: fall consecutive probe failures
Eligible --> Broken: 3 consecutive dial failures (passive breaker)
Broken --> Eligible: successful dial
Down --> Eligible: rise successes (no user traffic needed)
note right of Cold
ineligible until the rise
threshold is met
end note
note right of Down
all peers ineligible → 503
end note- Policies:
primary-first(default),weighted-round-robin(smooth WRR),weighted-least-active(active ÷ weight, smooth-WRR tie-break). These options on non-proxy processors, unknown policies and invalid health settings fail config loading rather than silently falling back. - Probes:
connecttests transport only;GET/HEADuse the configured protocol, validate TLS, bound the response head, compare the status exactly, follow no redirects, consume no body, and carry no cookies, authorization or body. Dedicated connections, a process-wide cap of 16 concurrent probes, skipped missed ticks; they never consume application pool permits. - All peers in a group share a protocol; do not put credentials, paths or queries in group authorities. A
Hostoverride does not override TLS identity. - Reloads: candidate generations start no probe tasks; publication starts the new scheduler and retires the old one. Unchanged identities keep their group state; changed endpoints, weights, policies or TLS identities start fresh. Server- and vhost-scoped processors are distinct.
- Reservations survive the response head: streaming bodies and WebSocket relays release the peer's active count on completion, error or drop.
Per-peer metrics carry scope, group name and ordinal labels (never URLs or credentials): httpjet_proxy_peer_{healthy,probes_total,health_transitions_total,active_requests,selections_total,prehead_failures_total}, plus httpjet_proxy_failover_total and the httpjet_phase_upstream_seconds latency histogram. The read-only status API exposes the same peer health.
Streaming, SSE and timeouts
Upstream responses are never buffered (respBuffer is parsed but always streams — check warns). Server-sent events therefore stay real-time through compression (the per-chunk CompressStream) and through H1, H2 and H3 alike. A backend connect timeout is initTimeout; the response-head deadline is the processor's default or a context's responseTimeout override; a backend timeout is a 504. Request bodies stream to h1/h2 upstreams; a context maxReqBodySize is enforced before dispatch.
WebSocket relay
A <websocketList><websocket> map (uri → address) or an upgrade request on a proxy context performs the HTTP/1 handshake with the backend and then relays raw frames both ways. The relay supports plain TCP HTTP/1 upstreams only — not TLS, H2 or Unix-socket targets. Maps carry a stable websocket:<uri> target identity scoped to their vhost, so named-target trace propagation applies; the client span covers the handshake, not the potentially unbounded relay. TCP keepalive keeps a vanished peer from pinning fds and permits forever.
FastCGI gateway opt-in
An external processor declared <type>fcgi</type> or fastcgi is an ExtKind::FastCgi — never silently converted into an HTTP proxy or an LSAPI worker. Only an explicit cgi script handler whose named processor is FastCGI dispatches through the gateway; a cgi mapping with a missing or different processor fails closed and is never served as static source. httpjet does not spawn the application: it connects to the configured TCP or Unix endpoint of an independently supervised process.
<extProcessor>
<name>my_fcgi</name>
<type>fcgi</type>
<address>uds:///run/my-app/fcgi.sock</address>
<maxConns>32</maxConns>
<initTimeout>5</initTimeout>
<pcKeepAliveTimeout>30</pcKeepAliveTimeout>
</extProcessor>
<scriptHandler>
<suffix>fcgi</suffix>
<type>cgi</type>
<handler>my_fcgi</handler>
</scriptHandler>END_REQUEST makes one reusable. Multiplexing request ids on one connection is disabled.diagram source (mermaid)
sequenceDiagram
participant P as pipeline<br/>suffix routing
participant H as hj-fastcgi handler
participant Pool as bounded pool<br/>(maxConns, one acquisition deadline)
participant A as FastCGI app<br/>(externally supervised, TCP or uds://)
P->>H: validated script target (beneath docroot, regular file, access checks passed)
H->>H: build CGI params — server-derived, HTTP_* validated, protected names not overridable
H->>H: buffer request body under the shared budget (exact STDIN length)
H->>Pool: acquire — poisoned by default
Pool->>A: BEGIN_REQUEST · PARAMS (≤65,535 B records) · STDIN · empty terminators
A-->>H: STDOUT: strict CGI head (CRLF only, one Status, Location default) + streamed body
A-->>H: STDERR — byte-capped, control-escaped → error log, never to client
A-->>H: END_REQUEST (exact 8-byte body)
H->>Pool: clean END_REQUEST → mark reusable — anything else → poison + close
H-->>P: response (hop-by-hop fields rejected)- Wire bounds: FastCGI version 1, nonzero request id, known record types, the protocol's 65,535-byte record limit; PARAMS lengths are rejected before allocation when either the field bound or the configured aggregate cap would be exceeded.
- Pool: caps all sockets including idle and in-progress dials under one acquisition deadline; poison/drop releases capacity immediately; clean idle sockets expire oldest-first.
- CGI variables are server-derived; request headers become
HTTP_*after validation; processorenvcannot override protected identity/routing variables (SCRIPT_FILENAME,DOCUMENT_ROOT,REMOTE_ADDR,SERVER_NAME,CONTENT_LENGTH,HTTP_AUTHORIZATION, …). The same deny-list applies to.htaccess[E=]for LSAPI. - Bodies are buffered under the shared budget so the exact STDIN length is known; declared underflow/overflow, framing errors and cancellation poison the connection.
- Responses: strict CRLF head, repeated end-to-end headers preserved, one valid
Status, CGILocationdefault, folding/hop-by-hop rejected; STDERR byte-capped and control-escaped into the error log, never the client.
Known follow-ups: explicit CGI context-directory routing (today: script-handler suffix mappings only), a codec fragmentation corpus, and a maintained real-application interoperability fixture.
Conventional CGI and uWSGI not routable
Both remain disabled by design until a demonstrated workload exists. Existing type=cgi, rails, appserver and unknown processor types never activate either; check --strict keeps warning for configured CGI contexts. The design contracts are strict on purpose: CGI would require an absolute execution root distinct from the docroot, a fixed non-root identity, wall/CPU/memory/process/output limits, an executable allowlist, direct execve with no shell or PATH lookup, inode pinning, and bounded pipes with process-group termination; uWSGI would be a separate crate over an explicit endpoint with modifier-0 packets only, no multiplexing, and no connection reuse until a maintained responder proves the response boundary. Proposed identities are type=cgi-exec and type=uwsgi.
Reproducible checks
cargo test --locked -p hj-config -p hj-proxy
cargo test --locked -p hj-proxy --test upstream_groups # h1/h2 weighted distribution, cold start, down/recovery, all-down 503
cargo test -p hj-fastcgi # synthetic loopback responder + clean-connection reuse
cargo test -p httpjet --bin httpjetTo return a group to legacy selection, remove loadBalancePolicy, addressWeights and healthCheck and reload; removing only the health check keeps the configured policy.
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.