Hmbown/CodeWhale · error · anyhow
MCP Streamable HTTP session expired; retry with a new…
Error message
MCP Streamable HTTP session expired; retry with a new session required ({detail}) What it means
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.
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.
Example fix
// before
let resp = transport.send(msg).await?; // fatal on session expiry
// after
match transport.send(msg).await {
Err(e) if e.to_string().contains("session expired") => {
transport.reconnect().await?;
transport.send(msg).await?
}
other => other?,
} Defensive patterns
Strategy: retry
Validate before calling
// on SessionExpired the transport clears session_id; detect and reconnect
let needs_new_session = matches!(err.downcast_ref::<StreamableSendError>(),
Some(StreamableSendError::SessionExpired(_))); Type guard
fn is_session_expired(err: &anyhow::Error) -> bool {
err.to_string().contains("session expired")
} Try / catch
match send_result {
Err(e) if e.to_string().contains("session expired") => {
transport.session_id = None; // already cleared by transport
reconnect().await?;
resend(msg).await?
}
other => other?,
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: Long-running TUI sessions left idle past the server's session TTL; server deploys/restarts mid-session; load-balanced MCP fleets without sticky sessions.
Related errors
- MCP server rejected the request with ; the session is no…
- MCP session expired
- MCP HTTP destination blocked by network policy
- MCP HTTP destination requires network approval
- MCP HTTP discovered URL must not contain credentials
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/8cd142b444096b68.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/mcp/http.rs:281
HttpTransportMode::Streamable(transport) => match transport.send(msg.clone()).await {
Ok(()) => Ok(()),
Err(StreamableSendError::Incompatible(detail)) => {
tracing::debug!(
"MCP Streamable HTTP unavailable; falling back to SSE endpoint discovery: {}",
detail
);
self.switch_to_sse_and_send(msg).await
}
Err(StreamableSendError::StaleSession(detail)) => {
if let HttpTransportMode::Streamable(transport) = &mut self.mode {
tracing::debug!(
target: "mcp",
error = %detail,
"MCP Streamable HTTP session expired; clearing cached session ID"
);
transport.session_id = None;
}
Err(anyhow::anyhow!(
"MCP Streamable HTTP session expired; retry with a new session required ({detail})"
))
}
Err(StreamableSendError::Other(err)) => Err(err),
},
HttpTransportMode::Sse(transport) => transport.send(msg).await,
}
}
async fn recv(&mut self) -> Result<Vec<u8>> {
match &mut self.mode {
HttpTransportMode::Streamable(transport) => transport.recv().await,
HttpTransportMode::Sse(transport) => transport.recv().await,
}
}
async fn shutdown(&mut self) {
if let HttpTransportMode::Sse(transport) = &mut self.mode {View on GitHub (pinned to 73e0f67d83)