You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
StreamableHTTPClientTransport doesn't handle 404 per spec (no session clear + re-init) #1708
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:
thrownewStreamableHTTPError(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.
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.
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.
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.
Activity
mbochneak commentedon Mar 19, 2026
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,
StreamableHTTPClientTransportneeds to handle it.Also worth noting:
_sessionIdpersists after theStreamableHTTPErrorthrow since there's no cleanup before the throw. This means subsequentsend()calls will also fail with the same stale session ID - it's a stuck state, not just a one-shot failure.Maverick-666 commentedon Mar 20, 2026
I’d like to work on this.
Plan:
Mcp-Session-Idis present.I’ll open a PR shortly.
Maverick-666 commentedon Mar 20, 2026
Implemented and opened a PR for this:
sessionExpired: truefor recoverable handling@modelcontextprotocol/clientPR: #1718
fix(client/streamableHttp): handle 404 by clearing session ID per MCP…
lanxevo3 commentedon Mar 28, 2026
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.
methran1304 commentedon Apr 28, 2026
What's the ETA on this?
ericmckinney commentedon Apr 28, 2026
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:
404 Not Foundlog lines on/mcprequests carrying anMcp-Session-Idthat the server had already evicted (typically after a server restart releases in-memory session state).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):
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 newInitializeRequestwithout 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
InitializeRequestcleanly. So at least empirically, the spec-violating behavior is implementation-specific to the connector that wrapsStreamableHTTPClientTransport(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
_sessionIdpast 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.
error.code-32001-32600(INVALID_REQUEST)idnull"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
InitializeRequestfrom 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.
Azazi commentedon Aug 13, 2026
Opened #2660, which rebases #2125's fix (thank you @dsp-ant for that work) onto current
mainto resolve its conflict with #2469.