Hmbown/CodeWhale · warning

MCP session preflight cancelled after plugin authority…

Error message

MCP session preflight cancelled after plugin authority changed

What it means

Before an MCP HTTP session is established, a preflight request resolves auth headers. Because a reviewed plugin's authority (trust/approval state) can change asynchronously, the preflight races it via `tokio::select!` with a cancellation token; if cancellation wins, the library aborts the session setup with this error instead of connecting to a server whose authority was revoked mid-flight.

Solutions

  1. Retry the connection after the plugin's authority state settles (the new policy will be applied on the next attempt)
  2. Complete any pending plugin review/approval so the authority stops changing, then reconnect
  3. Cancel and restart the MCP client cleanly rather than reusing the aborted transport
  4. If this happens repeatedly, serialize plugin policy changes with connection attempts in the caller

Example fix

// before
let transport = client.connect_with_policy(&policy, cancel).await?; // may abort with cancelled preflight
// after
match client.connect_with_policy(&policy, cancel).await {
    Ok(t) => t,
    Err(e) if e.to_string().contains("plugin authority changed") => {
        wait_for_policy_settled().await;
        client.connect_with_policy(&policy, cancel).await?
    }
    Err(e) => return Err(e),
}
Defensive patterns

Strategy: retry

Validate before calling

// Ensure no plugin policy reload is in flight before connecting
wait_for_plugin_policy_settled(&policy_watch).await;

Try / catch

match client.connect_with_policy(&policy, &cancel).await {
    Ok(t) => t,
    Err(e) if e.to_string().contains("plugin authority changed") => {
        tokio::time::sleep(Duration::from_millis(250)).await; // let policy settle, then retry
        client.connect_with_policy(&policy, &cancel).await?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: `try_establish_session` (crates/tui/src/mcp/http.rs:206) is waiting on `resolved_headers()` when the cancellation token fires — i.e. the plugin's reviewed authority changed (revoked/re-approval needed) during session setup, via `connect_with_policy` or a GET preflight.

Common situations: A user revokes or re-reviews a plugin while the client is still connecting; plugin policy is reloaded (config change) concurrently with a connection attempt; rapid connect/disconnect during plugin updates.

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 Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/c895ef78a7465f0e. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/mcp/http.rs:206

    ///   line and move on — the `initialize` POST will proceed without a
    ///   session ID.
    /// * If the server opens an SSE stream in response (the GET from old
    ///   SSE transport), we read only the headers, then discard the body
    ///   so the SSE stream is torn down. The actual SSE path uses a
    ///   dedicated `SseTransport` and is triggered by the incompatible-
    ///   status fallback in [`HttpTransport::send`].
    pub(super) async fn try_establish_session(&mut self) -> Result<()> {
        let cancel = self.cancel_token.clone();
        let transport = match &mut self.mode {
            HttpTransportMode::Streamable(t) => t,
            // Already on SSE — session is implicit via the long-lived GET.
            HttpTransportMode::Sse(_) => return Ok(()),
        };

        let headers = tokio::select! {
            biased;
            _ = cancel.cancelled() => {
                anyhow::bail!("MCP session preflight cancelled after plugin authority changed")
            }
            headers = transport.auth.resolved_headers() => headers?,
        };
        let request = apply_safe_custom_headers(
            with_default_mcp_http_headers(transport.client.get(&transport.url), false),
            &headers,
        );
        let response = tokio::select! {
            biased;
            _ = cancel.cancelled() => {
                anyhow::bail!("MCP session preflight cancelled after plugin authority changed")
            }
            response = tokio::time::timeout(Duration::from_secs(5), transport.client.send(request)) => {
                response
                    .map_err(|_| anyhow::anyhow!("GET timeout"))?
                    .map_err(|e| anyhow::anyhow!("GET error: {e}"))?
            }
        };

View on GitHub (pinned to 73e0f67d83)