{"record":{"id":"d79a892970dd1fb4","repo":"cloudflare/pingora","slug":"wrong-phase","errorCode":null,"errorMessage":"wrong phase {:?}","messagePattern":"wrong phase (.+?)","errorType":"panic","errorClass":"panic","httpStatus":null,"severity":"error","filePath":"pingora-cache/src/lib.rs","lineNumber":1844,"sourceCode":"    }\n\n    async fn purge_action(&self, action: PurgeAction) -> Result<bool> {\n        match self.phase {\n            CachePhase::CacheKey => {\n                let inner = self.inner();\n                let inner_enabled = self.inner_enabled();\n                let span = inner_enabled.traces.child(\"purge\");\n                let key = inner.key.as_ref().unwrap().to_compact();\n                Self::purge_impl(\n                    inner_enabled.storage,\n                    inner_enabled.eviction,\n                    &key,\n                    action,\n                    span,\n                )\n                .await\n            }\n            _ => panic!(\"wrong phase {:?}\", self.phase),\n        }\n    }\n\n    /// Delete the asset from the cache storage via a spawned task.\n    /// Returns corresponding `JoinHandle` of that task.\n    /// # Panic\n    /// Need to be called after the cache key is set. Panic otherwise.\n    pub fn spawn_async_purge(\n        &self,\n        context: &'static str,\n    ) -> tokio::task::JoinHandle<Result<bool>> {\n        if matches!(self.phase, CachePhase::Disabled(_) | CachePhase::Uninit) {\n            panic!(\"wrong phase {:?}\", self.phase);\n        }\n\n        let inner_enabled = self.inner_enabled();\n        let span = inner_enabled.traces.child(\"purge\");\n        let key = self.inner().key.as_ref().unwrap().to_compact();","sourceCodeStart":1826,"sourceCodeEnd":1862,"githubUrl":"https://github.com/cloudflare/pingora/blob/4487f7b2ab50f159e4a2cf4f6a6b813f61bb6e19/pingora-cache/src/lib.rs#L1826-L1862","documentation":"This panic comes from `purge_action` in pingora-cache, which dispatches on the `HttpCache` session's `phase`. Purging (via `purge()` or `expire()`) is only legal in the `CacheKey` phase — i.e., after a cache key has been set but before the cache phase has advanced. If called in any other phase (Disabled, Uninit, CacheMiss, CacheHit, CacheBypass, etc.), the library panics with `wrong phase {:?}` because the key/storage it needs is not available or valid at that point in the request lifecycle.","triggerScenarios":"Calling `session.cache.purge()` or `session.cache.expire()` (or `spawn_async_purge` when the phase is Disabled/Uninit) before `set_key()` has been called, or after the cache phase has already advanced past `CacheKey` (e.g., during miss filling, hit serving, or after caching was disabled/bypassed for the request).","commonSituations":"Developers implementing a cache-purge HTTP endpoint that calls purge on a request that never went through cache lookup; calling purge inside upstream_response or logging phases where the phase has moved on; requests where a cache filter disabled caching, leaving the phase Disabled while app code unconditionally purges.","solutions":["Ensure `set_key()` is called (via the `cache_key` request filter or manually) before calling `purge()`/`expire()`, and call those methods only during the CacheKey phase of the request.","Guard the call with `session.cache.phase()`/digest checks, or match on the phase and skip/warn instead of purging when it is not `CacheKey`.","If purge must happen outside the request flow, construct an independent cache miss (a fresh `HttpCache` with a key set) or use the storage API directly instead of reusing the request session.","For `spawn_async_purge`, verify the phase is not `Disabled` or `Uninit` before spawning."],"exampleFix":"// before\nasync fn handle_purge(session: &mut Session) {\n    session.cache.purge().await; // panics if key not set or phase advanced\n}\n// after\nasync fn handle_purge(session: &mut Session) {\n    if matches!(session.cache.phase(), CachePhase::CacheKey) {\n        session.cache.purge().await;\n    } else {\n        // skip or log: purge is only valid once the cache key is set,\n        // before the cache phase advances\n    }\n}","handlingStrategy":"validation","validationCode":"// Rust\nuse pingora_cache::{CachePhase, HttpCache};\nfn can_purge(cache: &HttpCache) -> bool {\n    matches!(cache.phase(), CachePhase::CacheKey)\n}\n// call purge()/expire() only if can_purge(&session.cache)","typeGuard":"fn is_cache_key_phase(cache: &HttpCache) -> bool {\n    matches!(cache.phase(), CachePhase::CacheKey)\n}","tryCatchPattern":null,"preventionTips":["Always set the cache key (via the `cache_key` filter) before any purge/expire call.","Only call purge/expire during request filters that run before cache lookup completes.","Add a debug assertion or log of the cache phase in dev to catch phase misuse early.","Treat purge/expire like lookup: part of the CacheKey-phase API, not usable later in the request."],"tags":["cache","panic","lifecycle","invalid-state"],"backgroundTag":"invalid-state-transition","analyzedSha":"4487f7b2ab50f159e4a2cf4f6a6b813f61bb6e19","analyzedAt":"2026-09-13T07:48:52.011Z","contentChangedAt":"2026-09-13T07:48:52.011Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}