Repository navigation
ClientDisconnect returns HTTP 500 #1648
Copy link
Copy link
Open
Labels
P2Moderate issues affecting some users, edge cases, potentially valuable featureModerate issues affecting some users, edge cases, potentially valuable featurebugSomething isn't workingSomething isn't workingready for workEnough information for someone to start working onEnough information for someone to start working on
Description
Initial Checks
- I confirm that I'm using the latest version of MCP Python SDK
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Description
StreamableHTTPServerTransport._handle_post_request in mcp/server/streamable_http.py incorrectly handles starlette.requests.ClientDisconnect exceptions.
Current behavior:
- Returns HTTP 500 (Internal Server Error)
- Logs as ERROR with full traceback
- Triggers production 5XX alerts
When This Occurs
ClientDisconnect happens during normal operations:
- Network timeouts
- User cancels request
- Load balancer timeouts
- Mobile client network interruptions
These are client-side events, not server failures.
Root Cause
File: src/mcp/server/streamable_http.py
Line: ~490-500
The broad except Exception handler catches ClientDisconnect and returns 500:
except Exception as err: # pragma: no cover
logger.exception("Error handling POST request") # ❌ Logs as ERROR
response = self._create_error_response(
f"Error handling POST request: {err}",
HTTPStatus.INTERNAL_SERVER_ERROR, # ❌ Returns 500
INTERNAL_ERROR,
)
await response(scope, receive, send)Example Code
Reproduction
Steps
1. Install MCP SDK:
python3 -m venv venv
source venv/bin/activate
pip install mcp
2. Create minimal_mcp_server.py based on the documentation:
#!/usr/bin/env python3
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Bug Demo", json_response=True)
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
if __name__ == "__main__":
mcp.run(transport="streamable-http")3. Create test_client_disconnect.py:
#!/usr/bin/env python3
import socket
import time
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("localhost", 8000))
# Send headers claiming 100KB body
headers = (
b"POST /mcp HTTP/1.1\r\n"
b"Host: localhost\r\n"
b"Content-Type: application/json\r\n"
b"Content-Length: 100000\r\n"
b"Accept: application/json, text/event-stream\r\n"
b"\r\n"
)
sock.send(headers)
# Send partial body then disconnect
sock.send(b'{"jsonrpc": "2.0", "method": "initialize", "params": {')
time.sleep(0.05)
sock.close()
print("✓ Client disconnect simulated")4. Run:
Terminal 1
python minimal_mcp_server.py
Terminal 2
python test_client_disconnect.py
5. Observe the bug in Terminal 1:
Error handling POST request
Traceback (most recent call last):
File ".../mcp/server/streamable_http.py", line 351, in _handle_post_request
body = await request.body()
^^^^^^^^^^^^^^^^^^^^
File ".../starlette/requests.py", line 243, in body
async for chunk in self.stream():
File ".../starlette/requests.py", line 237, in stream
raise ClientDisconnect()
starlette.requests.ClientDisconnect
Python & MCP Python SDK
Python 3.12.9, MCP Python SDK v1.21.2
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
P2Moderate issues affecting some users, edge cases, potentially valuable featureModerate issues affecting some users, edge cases, potentially valuable featurebugSomething isn't workingSomething isn't workingready for workEnough information for someone to start working onEnough information for someone to start working on
Activity
[-]ClientDisconnect returns HTTP 500 instead of 499[/-][+]ClientDisconnect returns HTTP 500[/+]FanisPapakonstantinou commentedon Nov 20, 2025
The most relevant HTTP status to return would be the non-standard 499 used by Nginx. 499 is not standard HTTP and so this attempt PR fails in the pyright step. We could use 499 with type: ignore, change function signature to accept int | HTTPStatus or use standard HTTP 408.
omar-y-abdi commentedon Mar 12, 2026
I'd like to take a look at this. The fix should catch
ClientDisconnectbefore the broadexcept Exceptionhandler — log it as a warning (since it's a client-side event, not a server error), and return early without sending any response (the client has already disconnected). Will have a PR up shortly.fix: handle ClientDisconnect gracefully in StreamableHTTP POST handler
fix(streamable_http): handle ClientDisconnect during POST at WARNING …
LarryHu0217 commentedon Aug 29, 2026
I reproduced this against current
mainby simulatingClientDisconnectwhile reading a Streamable HTTP POST body. The narrow behavior I validated is to catch the disconnect aroundrequest.body(), log it as a client-side warning, and return without constructing a response because the peer is already gone. A regression test also confirms the session remains usable for a subsequent request. I have a focused implementation and can submit it if a maintainer decides an outside PR is useful here.AI assistance disclosure: I used Codex to help inspect, implement, and test this change; I reviewed the diff and test results myself.
linhongyu510 commentedon Aug 30, 2026
I opened #3414 with a minimal fix for this: it catches
ClientDisconnectbefore the generic exception path, avoids reporting disconnected clients as 500 server errors, and adds an ASGI-level regression test.xianjianlf2 commentedon Aug 31, 2026
I’d like to work on this if the proposed behavior is still desired. My plan is to handle
starlette.requests.ClientDisconnectseparately in the Streamable HTTP POST path, avoid classifying a routine client disconnect as a server-side 500/error, and add a focused ASGI regression test that verifies the disconnect path does not emit an internal-error response.Disclosure: I use AI-assisted coding tools, and I will review, understand, and test the complete change before submitting it.
somanshreddy commentedon Sep 16, 2026
We hit this in production. During a period of elevated latency, clients started timing out and disconnecting while sending request bodies, and since each of those becomes a 500 they counted against our availability SLO as server faults. Our server-fault rate on POST went from about 0.05% to 1.6%, which against a 99.9% target is roughly 16x the error budget burn rate, and it paged. Essentially all of it was client disconnects rather than anything actually failing server-side.
Disclosure: I used AI tooling to investigate this. I hit the issue in production and reviewed this comment before posting.
Vinayak19112003 commentedon Sep 27, 2026
I can take this. One note from the history: the earlier fix #3414 was closed by the auto-close bot for lack of assignment, not on review feedback, so the approach was never evaluated on the maintainer side. I reproduced the disconnect path on current main —
ClientDisconnectraised fromawait request.body()in_handle_post_requestfalls into the broadexcept Exceptionand comes out as an HTTP 500 with an error-level log.My approach: catch
ClientDisconnectaround the body read, log it as a warning (client-side event, not a server error), and return without writing a response since the peer is already gone. The generic handler stays untouched for real server errors. I will add an ASGI-level regression test simulating a mid-body disconnect, asserting no 500/internal-error response and that the session still serves a subsequent request.Disclosure: I use AI-assisted coding tools to help implement; I review, understand, and test the full change myself and take responsibility for the result. Please assign me #1648.