Skip to content

ClientDisconnect returns HTTP 500 #1648

Description

@FanisPapakonstantinou

Initial Checks

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

Activity

  1. changed the title [-]ClientDisconnect returns HTTP 500 instead of 499[/-] [+]ClientDisconnect returns HTTP 500[/+] on Nov 20, 2025
  2. FanisPapakonstantinou commented on Nov 20, 2025

    @FanisPapakonstantinou
    Author

    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.

  3. added
    bugSomething isn't working
    ready for workEnough information for someone to start working on
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    on Dec 2, 2025
  4. omar-y-abdi commented on Mar 12, 2026

    @omar-y-abdi

    I'd like to take a look at this. The fix should catch ClientDisconnect before the broad except Exception handler — 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.

  5. LarryHu0217 commented on Aug 29, 2026

    @LarryHu0217

    I reproduced this against current main by simulating ClientDisconnect while reading a Streamable HTTP POST body. The narrow behavior I validated is to catch the disconnect around request.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.

  6. linhongyu510 commented on Aug 30, 2026

    @linhongyu510

    I opened #3414 with a minimal fix for this: it catches ClientDisconnect before the generic exception path, avoids reporting disconnected clients as 500 server errors, and adds an ASGI-level regression test.

  7. xianjianlf2 commented on Aug 31, 2026

    @xianjianlf2

    I’d like to work on this if the proposed behavior is still desired. My plan is to handle starlette.requests.ClientDisconnect separately 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.

  8. somanshreddy commented on Sep 16, 2026

    @somanshreddy

    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.

  9. Vinayak19112003 commented on Sep 27, 2026

    @Vinayak19112003

    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 — ClientDisconnect raised from await request.body() in _handle_post_request falls into the broad except Exception and comes out as an HTTP 500 with an error-level log.

    My approach: catch ClientDisconnect around 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.

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 workingready for workEnough information for someone to start working on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions