{"record":{"id":"b68ea0e789c895ae","repo":"cloudflare/pingora","slug":"cache-key-callback-must-be-implemented-when-cachin","errorCode":null,"errorMessage":"cache_key_callback must be implemented when caching is enabled","messagePattern":"cache_key_callback must be implemented when caching is enabled","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pingora-proxy/src/proxy_trait.rs","lineNumber":191,"sourceCode":"\n    /// This callback generates the cache key.\n    ///\n    /// This callback is called only when cache is enabled for this request.\n    ///\n    /// There is no sensible default cache key for all proxy applications. The\n    /// correct key depends on which request properties affect upstream responses\n    /// (e.g. `Vary` headers, custom request filters that modify the origin host).\n    /// Getting this wrong leads to cache poisoning.\n    ///\n    /// See `pingora-proxy/tests/utils/server_utils.rs` for a minimal (not\n    /// production-ready) reference implementation.\n    ///\n    /// # Panics\n    ///\n    /// The default implementation panics. You **must** override this method when\n    /// caching is enabled.\n    fn cache_key_callback(&self, _session: &Session, _ctx: &mut Self::CTX) -> Result<CacheKey> {\n        unimplemented!(\"cache_key_callback must be implemented when caching is enabled\")\n    }\n\n    /// This callback is invoked when a cacheable response is ready to be admitted to cache.\n    fn cache_miss(&self, session: &mut Session, _ctx: &mut Self::CTX) {\n        session.cache.cache_miss();\n    }\n\n    /// This filter is called after a successful cache lookup and before the\n    /// cache asset is ready to be used.\n    ///\n    /// This filter allows the user to log or force invalidate the asset, or\n    /// to adjust the body reader associated with the cache hit.\n    /// This also runs on stale hit assets (for which `is_fresh` is false).\n    ///\n    /// The value returned indicates if the force invalidation should be used,\n    /// and which kind. Returning `None` indicates no forced invalidation\n    async fn cache_hit_filter(\n        &self,","sourceCodeStart":173,"sourceCodeEnd":209,"githubUrl":"https://github.com/cloudflare/pingora/blob/0046038bd402bc82912da862dadf9a479f31e9f1/pingora-proxy/src/proxy_trait.rs#L173-L209","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: caching enabled in request_cache_filter but no cache_key_callback override\nimpl ProxyHttp for MyProxy {\n    fn request_cache_filter(&self, session: &mut Session, _ctx: &mut ()) -> Result<()> {\n        // ...enables caching via session.cache...\n        Ok(())\n    }\n}\n\n// after: add the required override\nfn cache_key_callback(&self, session: &Session, _ctx: &mut Self::CTX) -> Result<CacheKey> {\n    let req = session.req_header();\n    let host = req.headers.get(http::header::HOST).and_then(|v| v.to_str().ok()).unwrap_or(\"\");\n    let pq = req.uri.path_and_query().map(|p| p.as_str()).unwrap_or(\"/\");\n    Ok(CacheKey::new(format!(\"{host}{pq}\"), String::new()))\n}","handlingStrategy":"validation","validationCode":"// CI guard: drive one cacheable request through your ProxyHttp impl.\n// The default cache_key_callback panics, so this fails at test time, not in prod.\n#[tokio::test]\nasync fn cacheable_request_produces_cache_key() {\n    // enable caching exactly like request_cache_filter does, then issue a\n    // request through the proxy harness (see pingora-proxy/tests/utils/server_utils.rs)\n    // and assert a normal response instead of a panic/connection reset.\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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)"],"tags":["rust","pingora-proxy","cache","trait-method","panic"],"backgroundTag":"missing-required-trait-method","analyzedSha":"0046038bd402bc82912da862dadf9a479f31e9f1","analyzedAt":"2026-08-16T21:33:22.341Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}