cloudflare/pingora · error
cache_key_callback must be implemented when caching is enabl
Error message
cache_key_callback must be implemented when caching is enabled
What it means
ProxyHttp::cache_key_callback deliberately has no default implementation: a wrong cache key poisons the cache, so pingora refuses to guess and the default body panics with unimplemented!(). The callback is invoked by HttpProxy::proxy_cache as soon as session.cache.enabled() is true, i.e. once your request_cache_filter (or attached cache backend) enabled caching for that request. Hitting this panic means caching was turned on without porting the required key logic.
Source
Thrown at pingora-proxy/src/proxy_trait.rs:191
/// This callback generates the cache key.
///
/// This callback is called only when cache is enabled for this request.
///
/// There is no sensible default cache key for all proxy applications. The
/// correct key depends on which request properties affect upstream responses
/// (e.g. `Vary` headers, custom request filters that modify the origin host).
/// Getting this wrong leads to cache poisoning.
///
/// See `pingora-proxy/tests/utils/server_utils.rs` for a minimal (not
/// production-ready) reference implementation.
///
/// # Panics
///
/// The default implementation panics. You **must** override this method when
/// caching is enabled.
fn cache_key_callback(&self, _session: &Session, _ctx: &mut Self::CTX) -> Result<CacheKey> {
unimplemented!("cache_key_callback must be implemented when caching is enabled")
}
/// This callback is invoked when a cacheable response is ready to be admitted to cache.
fn cache_miss(&self, session: &mut Session, _ctx: &mut Self::CTX) {
session.cache.cache_miss();
}
/// This filter is called after a successful cache lookup and before the
/// cache asset is ready to be used.
///
/// This filter allows the user to log or force invalidate the asset, or
/// to adjust the body reader associated with the cache hit.
/// This also runs on stale hit assets (for which `is_fresh` is false).
///
/// The value returned indicates if the force invalidation should be used,
/// and which kind. Returning `None` indicates no forced invalidation
async fn cache_hit_filter(
&self,View on GitHub (pinned to 0046038bd4)
Solutions
- Override cache_key_callback in your ProxyHttp impl and return a key built from every request property that changes the upstream response (host, scheme, path, query, Vary-relevant headers)
- Model the override on pingora-proxy/tests/utils/server_utils.rs:697 (host + path_and_query) and extend it for your request filters
- If caching was enabled unintentionally, stop enabling it in request_cache_filter so session.cache.enabled() stays false
Example fix
// before: caching enabled in request_cache_filter but no cache_key_callback override
impl ProxyHttp for MyProxy {
fn request_cache_filter(&self, session: &mut Session, _ctx: &mut ()) -> Result<()> {
// ...enables caching via session.cache...
Ok(())
}
}
// after: add the required override
fn cache_key_callback(&self, session: &Session, _ctx: &mut Self::CTX) -> Result<CacheKey> {
let req = session.req_header();
let host = req.headers.get(http::header::HOST).and_then(|v| v.to_str().ok()).unwrap_or("");
let pq = req.uri.path_and_query().map(|p| p.as_str()).unwrap_or("/");
Ok(CacheKey::new(format!("{host}{pq}"), String::new()))
} Defensive patterns
Strategy: validation
Validate before calling
// CI guard: drive one cacheable request through your ProxyHttp impl.
// The default cache_key_callback panics, so this fails at test time, not in prod.
#[tokio::test]
async fn cacheable_request_produces_cache_key() {
// enable caching exactly like request_cache_filter does, then issue a
// request through the proxy harness (see pingora-proxy/tests/utils/server_utils.rs)
// and assert a normal response instead of a panic/connection reset.
} Prevention
- Treat cache_key_callback as mandatory the moment any code path can enable session.cache
- Keep an integration test that marks at least one request cacheable so a missing override breaks CI
- Build keys from host, scheme, path, query and Vary-relevant headers; never from client-supplied input alone (cache poisoning risk)
When it happens
Trigger: A ProxyHttp implementation enables caching for a request (request_cache_filter interacts with session.cache to enable it, proxy_trait.rs:161-172) but does not override cache_key_callback. The first cacheable request reaches proxy_cache.rs:56 and panics inside the default callback, killing that request task.
Common situations: Copying a proxy example and switching on the cache backend without porting the key callback; enabling cacheability per-request from config so tests on non-cacheable routes miss it; upgrading pingora where the reference implementation now lives in pingora-proxy/tests/utils/server_utils.rs.
Related errors
- take_write_lock() called without cache lock
- take_write_lock() called without lock
- non-pathname unix sockets not supported as peer
- Tried to listen with no addr specified
- invalid ca pem
AI-assisted analysis of cloudflare/pingora@0046038bd4 (2026-08-16).
Data as JSON: /api/errors/b68ea0e789c895ae.
Report an issue: GitHub.