Skip to content

StreamableHTTPClientTransport doesn't handle 404 per spec (no session clear + re-init) #1708

Open

Description

@mbochneak

The MCP spec (2025-03-26), Session Management point 4 says:

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.

Neither send() (POST) nor _startOrAuthSse() (GET) handle 404. Both paths just throw StreamableHTTPError. There's no session ID clearing and no re-initialization.

send() has retry logic for 401 (auth) and 403 (insufficient_scope), but 404 falls through to:

throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);

Since _sessionId isn't cleared before the throw, subsequent send() calls keep sending the stale session ID and keep getting 404. It's not a transient failure - the client is stuck until it's destroyed and recreated.

This means servers that correctly return 404 for expired/unknown session IDs (as #389 asks them to) permanently break SDK-based clients instead of triggering a graceful reconnect.

Expected behavior: On 404, clear _sessionId and either re-initialize the session automatically or surface an event that lets the Client layer handle re-initialization (since InitializeRequest involves capability negotiation that the transport doesn't own).

Ref: #389 tracks the server-side of this (examples returning 400 instead of 404). This is the client-side counterpart. Affects v1.26.0 and v1.27.1.

Activity

  1. mbochneak commented on Mar 19, 2026

    @mbochneak
    Author

    Related: #1678 fixes the server-side examples (returning 404 instead of 400 for invalid sessions). This issue is the client-side counterpart - once servers correctly return 404, StreamableHTTPClientTransport needs to handle it.

    Also worth noting: _sessionId persists after the StreamableHTTPError throw since there's no cleanup before the throw. This means subsequent send() calls will also fail with the same stale session ID - it's a stuck state, not just a one-shot failure.

  2. Maverick-666 commented on Mar 20, 2026

    @Maverick-666

    I’d like to work on this.

    Plan:

    • Reset/clear client session state on HTTP 404 when Mcp-Session-Id is present.
    • Ensure re-initialization flow is triggered according to spec.
    • Add regression tests for 404 -> new initialize behavior.

    I’ll open a PR shortly.

  3. Maverick-666 commented on Mar 20, 2026

    @Maverick-666

    Implemented and opened a PR for this:

    • Clears stale session state when a session-bound request gets HTTP 404 (POST + GET paths)
    • Tags the thrown SDK error with sessionExpired: true for recoverable handling
    • Adds regression tests for both paths
    • Includes a patch changeset for @modelcontextprotocol/client

    PR: #1718

  4. added a commit that references this issue on Mar 28, 2026
    77273a0
  5. lanxevo3 commented on Mar 28, 2026

    @lanxevo3

    Fixed in PR #1705 (actually #1805) — added 404 handling that clears the stale session ID before throwing, per MCP spec §Session Management. Added SdkErrorCode.ClientHttpNotFound for client-level detection.

  6. ghost added
    bugSomething isn't working
    on Apr 1, 2026
  7. ghost added
    ready for workEnough information for someone to start working on
    on Apr 1, 2026
  8. ghost added
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    on Apr 1, 2026
  9. methran1304 commented on Apr 28, 2026

    @methran1304

    What's the ETA on this?

  10. ericmckinney commented on Apr 28, 2026

    @ericmckinney

    Adding production field-evidence to support this issue, in case it's useful for prioritization or for the maintainers landing #1718.

    7-day production observation

    Operating a Python-SDK-based MCP server (Streamable HTTP transport, public via Cloudflare tunnel, bearer-auth at edge), we see this exact spec-violation pattern from at least one connector class consistently. Across a 7-day window:

    • 80+ 404 Not Found log lines on /mcp requests carrying an Mcp-Session-Id that the server had already evicted (typically after a server restart releases in-memory session state).
    • Per-incident retry distribution: 1, 2, 3, 4, 6, 7, 8, 9, 10, 12 retries before the client finally issues a fresh InitializeRequest (or otherwise rotates session). Distribution is not consistent with a constant retry-budget; appears time- or stream-bounded, but eventually terminates in every observed burst.

    Sample log entries (server side, redacted to non-secret fields):

    Stale session <hex> sent non-initialize request — returning 404
    INFO:     160.79.106.<x>:0 - "GET /mcp HTTP/1.1" 404 Not Found
    Stale session <hex> sent non-initialize request — returning 404
    INFO:     160.79.106.<x>:0 - "GET /mcp HTTP/1.1" 404 Not Found
    ... (repeat N times with same session id) ...
    INFO:     160.79.106.<x>:0 - "POST /mcp HTTP/1.1" 200 OK    # eventual reinit
    

    Spec reference (matches the issue): MCP 2025-06-18 §Session Management point 4 — "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." First 404 should be terminal-for-this-session-id; the observed retry-then-reinit pattern is non-conformant.

    Connector-class signal

    The 404 retry-bursts originate consistently from one Cloudflare egress class. A second connector class (a different MCP client wrapping localhost-direct, also Anthropic-built) hitting the same server does not trigger the bug — on session loss it issues InitializeRequest cleanly. So at least empirically, the spec-violating behavior is implementation-specific to the connector that wraps StreamableHTTPClientTransport (or an equivalent code path), not universal to all clients.

    This is consistent with the synthetic repro by @mcp-claude in the comment thread above: the SDK transport retains _sessionId past the throw and any wrapping client that doesn't manually destroy + recreate the transport stays stuck for as long as it tries.

    Possibly-related body-shape divergence (hypothesis, not validated)

    Cross-referencing python-sdk#1821: the JSON-RPC error body that servers emit on stale-session 404 differs across SDK implementations.

    TypeScript SDK Python SDK
    HTTP status 404 404
    error.code -32001 -32600 (INVALID_REQUEST)
    id null "server-error"

    If a TypeScript-based client classifies "stale session" by pattern-matching on the TypeScript-shape body (rather than HTTP 404 alone), a Python-server-emitted body might fall through to a generic-retry path rather than the stale-session reinit path. This is GUESSED, not observed — would need connector source-side or a controlled body-shape A/B to confirm — but worth flagging here in case it informs whether the SDK fix should also key on HTTP status alone (per spec) regardless of body shape.

    Server-side workaround (FYI)

    While #1708/#1718 lands, we've deployed a server-side auto-reinit middleware that synthesizes an InitializeRequest from the pre-authenticated bearer on stale-session non-initialize requests — feature-flagged and staged-rollout. It works around connector behavior at the cost of ~50 LOC of server middleware that we'd retire once the client-side fix is in. Mentioning only so the empirical motivation is visible: the bug is impacting real production traffic and folks are routing around it server-side.

    Happy to share more log samples, the retry-distribution methodology, or test against a candidate fix if useful for #1718's regression suite. Thanks for the work tracking this down.

  11. Azazi commented on Aug 13, 2026

    @Azazi

    Opened #2660, which rebases #2125's fix (thank you @dsp-ant for that work) onto current main to resolve its conflict with #2469.

  12. added
    v1Issues / PRs related to v1.x
    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes
    on Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featurebugSomething isn't workingfix proposedBot has a verified fix diff in the commentready for workEnough information for someone to start working onv1Issues / PRs related to v1.xv2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions