Life of a Request
On this page
This document traces a single HTTP request from arrival to response through the Praxis proxy.
Overview
- Pingora accepts the TCP connection and Praxis resolves TLS certificates via SNI.
- The listener’s protocol type determines the handler (HTTP or TCP).
- The handler loads a pipeline snapshot from
ArcSwap, pinned for this request’s lifetime. - Request filters execute forward (index 0 to N):
conditions are checked,
on_requestruns, branches are evaluated. - If any filter declares body access, request body chunks pass through body filters in forward order.
- The router filter sets
ctx.clusterand the load balancer setsctx.upstream, selecting the backend. - Pingora connects to the upstream, stripping hop-by-hop headers and injecting proxy headers.
- Response filters execute in reverse (index N to 0), processing only filters that ran during the request phase.
- Response body chunks pass through body filters in reverse order.
- Pingora sends the response to the client and returns the upstream connection to the pool.
The sections below expand each step. Operators can stop at the overview; the detail sections are for contributors and filter developers.
Step 1: Connection Accept
Pingora accepts the TCP connection on the listener’s
bound address. If TLS is configured, Praxis resolves
the certificate via SNI using ReloadableCertResolver
(crates/tls/src/), which supports hot-reload via ArcSwap.
Relevant files:
crates/tls/src/sni.rs: SNI resolutioncrates/tls/src/reload.rs: certificate hot-reloadcrates/protocol/src/http/pingora/handler/: HTTP handler
Step 2: Protocol Detection
The listener’s protocol field (default: http)
determines which protocol adapter handles the
connection. Each adapter implements the Protocol
trait (crates/protocol/src/lib.rs) and translates
Pingora callbacks into pipeline invocations.
HTTP listener --> Pingora HTTP handler
TCP listener --> Pingora TCP handler
An HTTP listener supports both HTTP and TCP filters. A TCP listener supports only TCP filters.
Step 3: Pipeline Snapshot
The protocol adapter loads the current pipeline from
Arc<ArcSwap<FilterPipeline>> via
ListenerPipelines::get(). The load() call returns
an Arc guard pinned for this request’s lifetime.
This is how hot reload works without disrupting
in-flight requests: a reload stores a new pipeline
into the ArcSwap. The next request loads the new
pointer, while requests already holding a guard
continue on the old pipeline.
Relevant files:
crates/protocol/src/pipelines.rs:ListenerPipelinescrates/server/src/reload.rs: reload orchestration
Step 4: Request Filter Execution
The pipeline executor (crates/filter/src/pipeline/http.rs)
runs a while-loop over the flat filter list:
idx = 0
while idx < filters.len():
check conditions → skip if unmet
run on_request
evaluate branches → adjust idx
For each filter:
- Condition check: if the filter has
whenorunlessconditions, they are evaluated against the request (path, method, headers). Unmet conditions skip the filter. on_request: the filter processes the request. It may setctx.cluster(router), setctx.upstream(load balancer), write filter results, inject headers, or reject the request.- Branch evaluation: if the filter has
branch_chains, each branch’son_resultcondition is checked againstctx.filter_results. The first matching branch fires and its rejoin target controls the loop index:
| Outcome | Effect |
|---|---|
Continue | Advance to next filter (idx + 1) |
SkipTo(target) | Jump forward to the target filter index |
ReEnter(target) | Loop back to the target index (re-entrance) |
Terminal | Stop the pipeline, proceed to upstream |
Reject(status) | Abort with an error response to the client |
Filter results are cleared after branch evaluation at each filter.
Relevant file: crates/filter/src/pipeline/http.rs
Step 5: Request Body Processing
Body processing only occurs if any filter in the
pipeline declared body access. The pipeline’s
BodyCapabilities (pre-computed at build time)
determines this.
Body delivery depends on BodyMode:
| Mode | Behavior |
|---|---|
Stream | Chunks forwarded immediately; filters see each chunk once |
StreamBuffer | Chunks buffered until the filter returns Release or end-of-stream |
SizeLimit | Like Stream, but enforces a maximum total size |
Body filters run in forward order. Filters that
returned BodyDone are skipped on subsequent chunks.
Relevant file: crates/filter/src/pipeline/http.rs
Step 6: Upstream Selection
Two filters collaborate to select the backend:
- Router (
routerfilter): matches the request path, host, and headers against configured routes. Setsctx.clusterto the winning cluster name. - Load balancer (
load_balancerfilter): selects an endpoint from the cluster using the configured strategy (round-robin, least connections, P2C, consistent hash). Setsctx.upstreamto the endpoint address.
The protocol adapter reads ctx.upstream to build
an HttpPeer for the Pingora connection.
Step 7: Upstream Request
Pingora connects to the upstream (or reuses a pooled connection). Before sending:
- Hop-by-hop headers are stripped (with conditional preservation for upgrade requests like WebSocket)
Hostheader is validatedX-Forwarded-For,X-Forwarded-Proto, andX-Forwarded-Hostare injected (if theforwarded_headersfilter ran)- Reserved internal headers (
x-praxis-*) are stripped
Retry logic handles idempotent failures based on the cluster’s retry configuration.
Relevant file:
crates/protocol/src/http/pingora/handler/upstream_peer.rs
Step 8: Response Filter Execution
Response filters execute in reverse order (last
filter first). Only filters that actually executed
during Step 4 run. Filters skipped by conditions
or SkipTo are also skipped in the response phase,
and in the request-body and response-body phases.
Each filter’s on_response receives the upstream
response headers and can modify them or reject the
response.
Response conditions (response_conditions on the
filter entry) can further gate execution based on
response status or headers.
Relevant file: crates/filter/src/pipeline/http.rs
Step 9: Response Body Processing
Response body filters run in reverse order,
using the same BodyMode logic as request body
processing. This phase is synchronous (a Pingora
constraint).
Filters that returned BodyDone during earlier
chunks are skipped.
Step 10: Response Delivery
Pingora sends the complete response to the client.
The upstream connection is returned to the
connection pool for reuse. The Arc guard on the
pipeline snapshot is released.
What Can Go Wrong
| Scenario | What happens |
|---|---|
Filter returns Reject | Pipeline stops; error response sent to client |
| Filter returns an error | Behavior depends on failure_mode: closed aborts, open logs and continues |
| No cluster set | 502 Bad Gateway (no router matched) |
| Upstream unreachable | Retry if idempotent, else 502 |
| Body exceeds size limit | 413 Payload Too Large |
| Pipeline validation fails at startup | Server refuses to start (unless skip_pipeline_validation is set) |
For production hardening, see Security Hardening.
Related
- Pipeline Concepts: mental model for chains, pipelines, naming
- Connection Lifecycle: Pingora-level sequence diagrams
- Payload Processing: body access, StreamBuffer, conditions
- Filter System: HttpFilter/TcpFilter traits, context fields
- Branch Chains: conditional branching in pipelines