openai/codex · error · StreamableHttpClientAdapterError

streamable HTTP session expired with 404 Not Found

Error message

streamable HTTP session expired with 404 Not Found

What it means

In the MCP Streamable HTTP transport the server hands out an Mcp-Session-Id after initialize; when a request carrying that id gets 404 Not Found (POST at http_client_adapter.rs:275, GET event stream at :562), the server no longer knows the session. The adapter maps that specifically to SessionExpired404 so rmcp can distinguish an expired session from a generic HTTP failure and re-run the initialize handshake instead of failing blindly.

Source

Thrown at codex-rs/rmcp-client/src/http_client_adapter.rs:106

}

struct EventStreamCancellation {
    request_id: RequestId,
    cancellations: Arc<Mutex<HashMap<RequestId, oneshot::Sender<()>>>>,
}

impl Drop for EventStreamCancellation {
    fn drop(&mut self) {
        self.cancellations
            .lock()
            .unwrap_or_else(PoisonError::into_inner)
            .remove(&self.request_id);
    }
}

#[derive(Debug, thiserror::Error)]
pub(crate) enum StreamableHttpClientAdapterError {
    #[error("streamable HTTP session expired with 404 Not Found")]
    SessionExpired404,
    #[error(transparent)]
    HttpRequest(#[from] ExecServerError),
    #[error("invalid HTTP header: {0}")]
    Header(String),
    #[error("MCP response body exceeds {maximum_bytes} bytes")]
    ResponseTooLarge { maximum_bytes: usize },
}

impl StreamableHttpClientAdapter {
    pub(crate) fn new(
        http_client: Arc<dyn HttpClient>,
        default_headers: HeaderMap,
        auth_provider: Option<SharedAuthProvider>,
        has_configured_headers: bool,
        redirect_mode: StreamableHttpRedirectMode,
        initialize_deadline: Arc<Mutex<Option<Instant>>>,
    ) -> Self {

View on GitHub (pinned to 339751715c)

Solutions

  1. Catch the error and re-run the MCP initialize handshake to obtain a fresh session id, then retry the request once
  2. If you deploy the server, enable sticky sessions or share session state across instances
  3. Check/increase the server's session TTL if clients legitimately stay idle
  4. Emit periodic pings/keepalives so sessions do not expire during idle periods

Example fix

// before
let result = client.call_tool("search", args).await?; // SessionExpired404 bubbles

// after
match client.call_tool("search", args).await {
    Ok(result) => Ok(result),
    Err(e) if is_session_expired(&e) => {
        let client = reinitialize_mcp_http_client(&config).await?;
        client.call_tool("search", args).await
    }
    Err(e) => Err(e),
}
Defensive patterns

Strategy: retry

Type guard

fn is_session_expired(error: &rmcp::service::ServiceError) -> bool {
    error.to_string().contains("session expired with 404")
}

Try / catch

// 404 on a known session id: re-initialize once, then retry
match client.call_tool(name, args).await {
    Ok(result) => Ok(result),
    Err(e) if e.to_string().contains("session expired with 404") => {
        let client = reinitialize_mcp_http_client(&config).await?;
        client.call_tool(name, args).await
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: post_message() with a session_id receiving HTTP 404, or get_stream() receiving 404 — typically after the server's session TTL elapses, the server restarts, or the session was never stored on the node that answered.

Common situations: MCP server restart or redeploy under a long-lived client; idle sessions exceeding the server TTL; load balancers without sticky sessions routing to a different instance; resuming a client after laptop sleep/hibernate; servers that persist sessions in memory only.

Related errors


AI-assisted analysis of openai/codex@339751715c (2026-08-25). Data as JSON: /api/errors/30cdf2d8a8e501d1. Report an issue: GitHub.