{"record":{"id":"c895ef78a7465f0e","repo":"Hmbown/CodeWhale","slug":"mcp-session-preflight-cancelled-after-plugin-autho","errorCode":null,"errorMessage":"MCP session preflight cancelled after plugin authority changed","messagePattern":"MCP session preflight cancelled after plugin authority changed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/tui/src/mcp/http.rs","lineNumber":206,"sourceCode":"    ///   line and move on — the `initialize` POST will proceed without a\n    ///   session ID.\n    /// * If the server opens an SSE stream in response (the GET from old\n    ///   SSE transport), we read only the headers, then discard the body\n    ///   so the SSE stream is torn down. The actual SSE path uses a\n    ///   dedicated `SseTransport` and is triggered by the incompatible-\n    ///   status fallback in [`HttpTransport::send`].\n    pub(super) async fn try_establish_session(&mut self) -> Result<()> {\n        let cancel = self.cancel_token.clone();\n        let transport = match &mut self.mode {\n            HttpTransportMode::Streamable(t) => t,\n            // Already on SSE — session is implicit via the long-lived GET.\n            HttpTransportMode::Sse(_) => return Ok(()),\n        };\n\n        let headers = tokio::select! {\n            biased;\n            _ = cancel.cancelled() => {\n                anyhow::bail!(\"MCP session preflight cancelled after plugin authority changed\")\n            }\n            headers = transport.auth.resolved_headers() => headers?,\n        };\n        let request = apply_safe_custom_headers(\n            with_default_mcp_http_headers(transport.client.get(&transport.url), false),\n            &headers,\n        );\n        let response = tokio::select! {\n            biased;\n            _ = cancel.cancelled() => {\n                anyhow::bail!(\"MCP session preflight cancelled after plugin authority changed\")\n            }\n            response = tokio::time::timeout(Duration::from_secs(5), transport.client.send(request)) => {\n                response\n                    .map_err(|_| anyhow::anyhow!(\"GET timeout\"))?\n                    .map_err(|e| anyhow::anyhow!(\"GET error: {e}\"))?\n            }\n        };","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/mcp/http.rs#L188-L224","documentation":"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.","triggerScenarios":"`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.","commonSituations":"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.","solutions":["Retry the connection after the plugin's authority state settles (the new policy will be applied on the next attempt)","Complete any pending plugin review/approval so the authority stops changing, then reconnect","Cancel and restart the MCP client cleanly rather than reusing the aborted transport","If this happens repeatedly, serialize plugin policy changes with connection attempts in the caller"],"exampleFix":"// before\nlet transport = client.connect_with_policy(&policy, cancel).await?; // may abort with cancelled preflight\n// after\nmatch client.connect_with_policy(&policy, cancel).await {\n    Ok(t) => t,\n    Err(e) if e.to_string().contains(\"plugin authority changed\") => {\n        wait_for_policy_settled().await;\n        client.connect_with_policy(&policy, cancel).await?\n    }\n    Err(e) => return Err(e),\n}","handlingStrategy":"retry","validationCode":"// Ensure no plugin policy reload is in flight before connecting\nwait_for_plugin_policy_settled(&policy_watch).await;","typeGuard":null,"tryCatchPattern":"match client.connect_with_policy(&policy, &cancel).await {\n    Ok(t) => t,\n    Err(e) if e.to_string().contains(\"plugin authority changed\") => {\n        tokio::time::sleep(Duration::from_millis(250)).await; // let policy settle, then retry\n        client.connect_with_policy(&policy, &cancel).await?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Avoid revoking/re-reviewing plugins while connections are being established","Serialize plugin policy reloads with client connect attempts","Treat this error as 'state changed, retry' rather than a hard failure"],"tags":["mcp","cancellation","race","plugin"],"backgroundTag":"invalid-state-transition","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}