cloudflare/pingora
Proxy phases
The proxy framework is a state machine. At each "phase" your ProxyHttp impl gets a chance to inspect or mutate state. This page is the reference for what every callback does and when it fires.
The narrative version is at docs/user_guide/phase.md. The trait definitions are in pingora-proxy/src/proxy_trait.rs.
The diagram
graph TD
start("new request")-->early_request_filter
early_request_filter-->request_filter
request_filter-->upstream_peer
upstream_peer-->Connect{IO: connect to upstream}
Connect-->|connection success|connected_to_upstream
Connect-->|connection failure|fail_to_connect
connected_to_upstream-->upstream_request_filter
upstream_request_filter-->request_body_filter
request_body_filter-->SendReq{IO: send request}
SendReq-->RecvResp{IO: read response}
RecvResp-.feature: adjust_upstream_modules.->adjust_upstream_modules
adjust_upstream_modules-->upstream_response_filter
upstream_response_filter-->response_filter
response_filter-->upstream_response_body_filter
upstream_response_body_filter-->response_body_filter
response_body_filter-->logging
logging-->endreq("request done")
fail_to_connect-->|can retry|upstream_peer
fail_to_connect-->|can't retry|fail_to_proxy
fail_to_proxy-->logging
request_filter-->|send response|logging
Error[response filter error]-->error_while_proxy
IOFailure[IO error]-->error_while_proxy
error_while_proxy-->|can retry|upstream_peer
error_while_proxy-->|can't retry|fail_to_proxyGeneral rules
- Most filters return
pingora_error::Result<T>. AnErrshort-circuits tofail_to_proxy()and ends the request. - All filters are async.
- A per-request
CTXis shared across every callback; it's mutable. - Most filters are optional with no-op defaults. The only required method is
upstream_peer(). upstream_response_*andresponse_*are paired; theupstream_*versions run before HTTP caching and changes affect the cached copy. The non-upstream_*versions run before sending to the downstream.
The phases
early_request_filter()
First chance to inspect a request. Runs before downstream modules. Use this for module configuration that needs to happen before any module looks at the request.
request_filter()
The main pre-upstream phase. Validate inputs, rate-limit, initialize CTX, optionally short-circuit by writing a response to the downstream and returning early.
request_body_filter()
Called every time a chunk of the request body arrives. Use this if your proxy needs to inspect or transform the request body.
proxy_upstream_filter()
Returns a bool. If false, the proxy skips going upstream and returns 502 by default. Use this for "I want to serve a synthetic response" patterns.
upstream_peer()
The only required callback. Returns a Box<HttpPeer> describing where to send the request and how. Most load-balancing logic lives here. Re-runs on retries.
connected_to_upstream()
Fires after a successful connect. Receives connection info (RTT, TLS digest, etc.). Used for logging and metrics.
fail_to_connect()
Fires on connect failure. Decide whether the error is retryable. If set_retry(true), upstream_peer() is called again — you can pick a different peer.
upstream_request_filter()
Modify the request headers just before they go on the wire. Useful for adding Host, custom auth headers, removing internal hop-by-hop headers, etc. The README example uses this to inject a Host header.
adjust_upstream_modules() (feature: adjust_upstream_modules)
Fires when the upstream response header arrives, before upstream modules process it. You can configure module behavior based on the response — e.g. supply a compression dictionary keyed on the response content. Read-only access to the response header here; use upstream_response_filter to mutate it.
upstream_response_filter() / upstream_response_body_filter() / upstream_response_trailer_filter()
Mutate the response on the way back from the upstream, before the cache layer sees it. Changes here are persisted in the cache.
response_filter() / response_body_filter() / response_trailer_filter()
Mutate the response on the way to the downstream, after the cache layer. Changes here are not persisted.
error_while_proxy()
Fires when an in-flight error happens during proxying after a connection was established. Decide whether to retry. Idempotent methods (GET, HEAD, etc.) are usually retried; non-idempotent are not.
fail_to_proxy()
The catch-all error path. Returns a FailToProxy describing the response to send and whether to suppress the error log. Standard pattern: send a 502 with a body explaining the failure class.
logging()
Always runs as the last phase, success or failure. Use for access logs, metrics, span finalization.
Helpers (not phases)
| Helper | What it does |
|---|---|
request_summary() |
One-line description of the request. Used in error logs and traces. |
suppress_error_log() |
Return true to skip the default error log line for this error. Useful for spammy downstream errors. |
init_downstream_modules() |
Build the per-session module set. |
cache_key_callback() |
Build the cache key from the request. |
cache_lookup_filter() |
Final cacheability decision. |
Cache phases (additional)
When the cache feature is on, additional callbacks fire around the lookup and fill:
cache_key_callbackcache_lookup_filtercache_hit_filtercache_missshould_serve_stalecache_not_modified_filtercache_revalidate_filterrange_header_filter
These are all in pingora-proxy/src/proxy_trait.rs. Their semantics are documented inline.
Where the calls live
The wire-format implementations call these callbacks at the right points:
| Phase | Called from |
|---|---|
early_request_filter |
proxy_h1.rs::process_request_header, proxy_h2.rs::process_request_header |
upstream_peer |
proxy_common.rs::find_upstream_peer |
upstream_request_filter |
proxy_h1.rs, proxy_h2.rs (just before write) |
| Cache callbacks | proxy_cache.rs |
fail_to_proxy |
proxy_common.rs::fail_to_proxy |
When debugging, set a breakpoint at the relevant process_* function and step through.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.