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

  1. 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.
  2. 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`.
  3. 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.
  4. 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

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


AI-assisted analysis of cloudflare/pingora@4487f7b2ab (2026-09-13). Data as JSON: /api/errors/d79a892970dd1fb4. Report an issue: GitHub.