Skip to content

Streamable HTTP: Session not found returns 401 instead of 404 (MCP spec violation) #689

@Ejb503

Description

@Ejb503

Summary

When a client sends a request with an Mcp-Session-Id header for a session that no longer exists (expired, server restarted, etc.), the StreamableHttpService returns HTTP 401 Unauthorized instead of HTTP 404 Not Found.

The MCP specification (2025-03-26) requires 404:

"The server MAY terminate the session at any time, after which it MUST respond to requests containing that session ID with HTTP 404 Not Found."

"When a client receives HTTP 404 in response to a request containing an Mcp-Session-Id, it MUST start a new session by sending a new InitializeRequest without a session ID attached."

Location

crates/rmcp/src/transport/streamable_http_server/tower.rs

handle_get (lines ~203-208):

if !has_session {
    // unauthorized
    return Ok(Response::builder()
        .status(http::StatusCode::UNAUTHORIZED)
        .body(Full::new(Bytes::from("Unauthorized: Session not found")).boxed())
        .expect("valid response"));
}

handle_post (lines ~315-320):

if !has_session {
    // unauthorized
    return Ok(Response::builder()
        .status(http::StatusCode::UNAUTHORIZED)
        .body(Full::new(Bytes::from("Unauthorized: Session not found")).boxed())
        .expect("valid response"));
}

resume errors (line ~222):

internal_error_response("resume session") returns HTTP 500 when SessionManager::resume() fails. If resume fails because the session is gone, this should also be 404.

Expected Behavior

When has_session() returns false for a session ID that was previously valid:

  • Return HTTP 404 Not Found (not 401)
  • This tells the client to start a new session with a fresh InitializeRequest

When resume() returns an error because the session is not resumable:

  • Return HTTP 404 Not Found (not 500)
  • Same rationale: the session is gone, client should create a new one

Impact

Clients that strictly follow the MCP spec only check for 404 to trigger session re-creation. Receiving 401 or 500 instead may cause:

  • Retry loops (client retries the same dead session instead of creating a new one)
  • Connection storms (multiple reconnection attempts all failing with 500)

Suggested Fix

// In handle_get and handle_post, when has_session returns false:
if !has_session {
    return Ok(Response::builder()
        .status(http::StatusCode::NOT_FOUND)
        .body(Full::new(Bytes::from("Session not found")).boxed())
        .expect("valid response"));
}

For resume failures, consider distinguishing "session not found" errors from true internal errors, mapping the former to 404.

Spec Reference

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High: significant functionality gap or spec violationT-transportTransport layer changesbugSomething is not workingready for workIssue is well-defined and ready to be picked up

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions