cloudflare/pingora · error · panic
wrong phase {:?}
Error message
wrong phase {:?} What it means
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.
Source
Thrown at pingora-cache/src/lib.rs:1844
}
async fn purge_action(&self, action: PurgeAction) -> Result<bool> {
match self.phase {
CachePhase::CacheKey => {
let inner = self.inner();
let inner_enabled = self.inner_enabled();
let span = inner_enabled.traces.child("purge");
let key = inner.key.as_ref().unwrap().to_compact();
Self::purge_impl(
inner_enabled.storage,
inner_enabled.eviction,
&key,
action,
span,
)
.await
}
_ => panic!("wrong phase {:?}", self.phase),
}
}
/// Delete the asset from the cache storage via a spawned task.
/// Returns corresponding `JoinHandle` of that task.
/// # Panic
/// Need to be called after the cache key is set. Panic otherwise.
pub fn spawn_async_purge(
&self,
context: &'static str,
) -> tokio::task::JoinHandle<Result<bool>> {
if matches!(self.phase, CachePhase::Disabled(_) | CachePhase::Uninit) {
panic!("wrong phase {:?}", self.phase);
}
let inner_enabled = self.inner_enabled();
let span = inner_enabled.traces.child("purge");
let key = self.inner().key.as_ref().unwrap().to_compact();View on GitHub (pinned to 4487f7b2ab)
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.
Example fix
// before
async fn handle_purge(session: &mut Session) {
session.cache.purge().await; // panics if key not set or phase advanced
}
// after
async fn handle_purge(session: &mut Session) {
if matches!(session.cache.phase(), CachePhase::CacheKey) {
session.cache.purge().await;
} else {
// skip or log: purge is only valid once the cache key is set,
// before the cache phase advances
}
} Defensive patterns
Strategy: validation
Validate before calling
// Rust
use pingora_cache::{CachePhase, HttpCache};
fn can_purge(cache: &HttpCache) -> bool {
matches!(cache.phase(), CachePhase::CacheKey)
}
// call purge()/expire() only if can_purge(&session.cache) Type guard
fn is_cache_key_phase(cache: &HttpCache) -> bool {
matches!(cache.phase(), CachePhase::CacheKey)
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- cache_key_callback must be implemented when caching is enabl
- non-pathname unix sockets not supported as peer
- Tried to listen with no addr specified
- non-pathname unix sockets not supported as peer
- failed to build tokio runtime for parent signal wait
AI-assisted analysis of cloudflare/pingora@4487f7b2ab (2026-09-13).
Data as JSON: /api/errors/d79a892970dd1fb4.
Report an issue: GitHub.