{"record":{"id":"0bc57f2bc6db5b63","repo":"Hmbown/CodeWhale","slug":"mcp-session-expired-transport-sse-endpoint-sta","errorCode":null,"errorMessage":"MCP session expired (transport=sse endpoint={} status={}): {}","messagePattern":"MCP session expired \\(transport=sse endpoint=(.+?) status=(.+?)\\): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp/sse.rs","lineNumber":299,"sourceCode":"            with_default_mcp_http_headers(self.client.post(&endpoint), true),\n            &headers,\n        )\n        .body(msg)\n        .send()\n        .await\n        .with_context(|| {\n            format!(\n                \"MCP SSE POST send failed (transport=sse endpoint={})\",\n                mask_url_secrets(&endpoint)\n            )\n        })?;\n        let status = response.status();\n        if !status.is_success() {\n            let body_excerpt = bounded_body_excerpt(response, ERROR_BODY_PREVIEW_BYTES).await;\n            let stale_session = is_mcp_stale_session_body(&body_excerpt);\n            let body_excerpt = self.auth.server_error_preview(&body_excerpt);\n            if stale_session {\n                anyhow::bail!(\n                    \"MCP session expired (transport=sse endpoint={} status={}): {}\",\n                    mask_url_secrets(&endpoint),\n                    status,\n                    body_excerpt\n                );\n            }\n            anyhow::bail!(\n                \"MCP SSE POST rejected (transport=sse endpoint={} status={}): {}\",\n                mask_url_secrets(&endpoint),\n                status,\n                body_excerpt\n            );\n        }\n        Ok(())\n    }\n\n    async fn recv(&mut self) -> Result<Vec<u8>> {\n        loop {","sourceCodeStart":281,"sourceCodeEnd":317,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp/sse.rs#L281-L317","documentation":"A POST to the SSE message endpoint failed and the body matched the stale-session classifier: is_mcp_stale_session_body() (crates/tui/src/mcp/wire.rs) looks for 'session' plus 'expired' or 'invalid', case-insensitive. The server no longer recognizes the session ID — it restarted, expired the session, or the POST landed on an instance without it. The connection must be re-established and re-initialized.","triggerScenarios":"The MCP SSE server restarts (deploy, crash) while the client holds an old session ID; server-side session TTL expiry on a long-lived connection; a load balancer without sticky sessions routing the POST to a different instance.","commonSituations":"Server upgraded mid-session; multi-instance deployments missing session affinity; idle connections outliving the server's session TTL.","solutions":["Reconnect: tear down the transport and redo connect + initialize to obtain a fresh endpoint and session","Rely on the client's stale-session reconnect handling (wire.rs is_mcp_stale_session_error) instead of retrying the same POST","If frequent, increase the server's session TTL or add sticky sessions for the messages endpoint"],"exampleFix":"// before: retrying the same POST keeps failing\ntransport.send(request).await?;\n\n// after: on stale session, reconnect and re-initialize\nmatch transport.send(request).await {\n    Err(e) if is_mcp_stale_session_error(&e) => {\n        transport = reconnect(&config).await?; // fresh endpoint event + session\n        initialize(&mut transport).await?;\n        transport.send(request).await?\n    }\n    other => other?,\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"```rust\n// crates/tui/src/mcp/wire.rs already ships this classifier:\n// is_mcp_stale_session_error(&err) matches \"MCP session expired\" and friends.\nfn is_stale_sse_session(err: &anyhow::Error) -> bool {\n    err.to_string().starts_with(\"MCP session expired (transport=sse\")\n}\n```","tryCatchPattern":"```rust\nmatch transport.send(payload).await {\n    Err(e) if is_stale_sse_session(&e) => {\n        transport = reconnect(&cfg).await?;   // fresh endpoint event + session\n        initialize(&mut transport).await?;    // re-run initialize handshake\n        transport.send(payload).await\n    }\n    other => other,\n}\n```","preventionTips":["Keep client state re-initializable so reconnects across server deploys are cheap","Use sticky sessions when running multiple MCP server instances behind a load balancer"],"tags":["mcp","sse","session","reconnection"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}