{"record":{"id":"8cd142b444096b68","repo":"Hmbown/CodeWhale","slug":"mcp-streamable-http-session-expired-retry-with-a","errorCode":null,"errorMessage":"MCP Streamable HTTP session expired; retry with a new session required ({detail})","messagePattern":"MCP Streamable HTTP session expired; retry with a new session required \\((.+?)\\)","errorType":"exception","errorClass":"anyhow","httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp/http.rs","lineNumber":281,"sourceCode":"            HttpTransportMode::Streamable(transport) => match transport.send(msg.clone()).await {\n                Ok(()) => Ok(()),\n                Err(StreamableSendError::Incompatible(detail)) => {\n                    tracing::debug!(\n                        \"MCP Streamable HTTP unavailable; falling back to SSE endpoint discovery: {}\",\n                        detail\n                    );\n                    self.switch_to_sse_and_send(msg).await\n                }\n                Err(StreamableSendError::StaleSession(detail)) => {\n                    if let HttpTransportMode::Streamable(transport) = &mut self.mode {\n                        tracing::debug!(\n                            target: \"mcp\",\n                            error = %detail,\n                            \"MCP Streamable HTTP session expired; clearing cached session ID\"\n                        );\n                        transport.session_id = None;\n                    }\n                    Err(anyhow::anyhow!(\n                        \"MCP Streamable HTTP session expired; retry with a new session required ({detail})\"\n                    ))\n                }\n                Err(StreamableSendError::Other(err)) => Err(err),\n            },\n            HttpTransportMode::Sse(transport) => transport.send(msg).await,\n        }\n    }\n\n    async fn recv(&mut self) -> Result<Vec<u8>> {\n        match &mut self.mode {\n            HttpTransportMode::Streamable(transport) => transport.recv().await,\n            HttpTransportMode::Sse(transport) => transport.recv().await,\n        }\n    }\n\n    async fn shutdown(&mut self) {\n        if let HttpTransportMode::Sse(transport) = &mut self.mode {","sourceCodeStart":263,"sourceCodeEnd":299,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/mcp/http.rs#L263-L299","documentation":"When a Streamable HTTP send fails with `StreamableSendError::SessionExpired`, the transport clears its cached session ID and returns this error telling the caller the whole message must be retried with a fresh session. The server invalidated the session (idle expiry, restart), so the old `Mcp-Session-Id` header is no longer valid.","triggerScenarios":"Sending a JSON-RPC message to an MCP Streamable HTTP server after the session expired: server restarted, session timed out server-side, or the server rolled back to a load balancer replica without the session.","commonSituations":"Long-running TUI sessions left idle past the server's session TTL; server deploys/restarts mid-session; load-balanced MCP fleets without sticky sessions.","solutions":["Reconnect (retry the connection) so a new session is established, then resend the failed message.","Enable keep-alive/heartbeat traffic to avoid idle session expiry.","Configure the MCP server for longer session TTLs or sticky sessions on the load balancer.","Ensure the calling engine treats this error as retry-with-new-session rather than a fatal failure."],"exampleFix":"// before\nlet resp = transport.send(msg).await?; // fatal on session expiry\n// after\nmatch transport.send(msg).await {\n    Err(e) if e.to_string().contains(\"session expired\") => {\n        transport.reconnect().await?;\n        transport.send(msg).await?\n    }\n    other => other?,\n}","handlingStrategy":"retry","validationCode":"// on SessionExpired the transport clears session_id; detect and reconnect\nlet needs_new_session = matches!(err.downcast_ref::<StreamableSendError>(),\n    Some(StreamableSendError::SessionExpired(_)));","typeGuard":"fn is_session_expired(err: &anyhow::Error) -> bool {\n    err.to_string().contains(\"session expired\")\n}","tryCatchPattern":"match send_result {\n    Err(e) if e.to_string().contains(\"session expired\") => {\n        transport.session_id = None; // already cleared by transport\n        reconnect().await?;\n        resend(msg).await?\n    }\n    other => other?,\n}","preventionTips":["Send periodic keep-alive traffic to avoid idle session expiry","Treat session-expired as retry-with-new-session in the engine, never fatal","Use sticky sessions or long TTLs on load-balanced MCP servers"],"tags":["http","session","mcp"],"backgroundTag":"jwt-token-expired","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"}