modelcontextprotocol / modelcontextprotocol/python-sdk

StreamableHTTP: a client disconnect while reading the POST body is logged as an exception and answered 500

Ouverte Adaptée aux débutants
#3,469 2 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

v1 v2
Langage dominant
Python
Étoiles
24.3k
Forks
4k
Merge moyen
1 j 1 h
PR mergées (30 j)
31

Description

Initial Checks
Release line

2.x (current stable), and the same on 1.28.1

Description

StreamableHTTPServerTransport._handle_post_request reads the request body inside its blanket except Exception. When the client goes away before the body arrives, await request.body() raises starlette.requests.ClientDisconnect, the transport logs it with logger.exception("Error handling POST request") and answers 500. That is a normal network event on a public MCP endpoint (mobile clients, proxies with short timeouts, health probes), and it lands as an unhandled server error with a stack trace in every log aggregator. #2064 / #2257 handled the disconnect in _handle_message; this is the earlier point, in streamable_http.py (the body = await request.body() line and the except Exception as err below it). The SSE transport has no handling for it either.

Example Code
import asyncio
import importlib.metadata as md
import logging

from mcp.server.streamable_http import StreamableHTTPServerTransport

records = []


class Capture(logging.Handler):
    def emit(self, record):
        records.append(record)


logging.getLogger("mcp").addHandler(Capture())
logging.getLogger("mcp").setLevel(logging.DEBUG)


async def main():
    transport = StreamableHTTPServerTransport(mcp_session_id=None)
    scope = {
        "type": "http", "method": "POST", "path": "/mcp", "query_string": b"", "headers": [
            (b"content-type", b"application/json"),
            (b"accept", b"application/json, text/event-stream"),
            (b"content-length", b"58"),
        ],
    }

    async def receive():  # the client went away before sending its body
        return {"type": "http.disconnect"}

    sent = []

    async def send(message):
        sent.append(message)

    async with transport.connect():
        try:
            await asyncio.wait_for(transport.handle_request(scope, receive, send), timeout=5)
        except asyncio.TimeoutError:
            print("handle_request did not return within 5s")
    status = next((m["status"] for m in sent if m["type"] == "http.response.start"), None)
    print(f"mcp {md.version('mcp')}: response status = {status}")
    for r in records:
        if r.levelno >= logging.ERROR:
            print(f"  logged {r.levelname} by {r.name}: {r.getMessage()} | exc: {type(r.exc_info[1]).__name__ if r.exc_info else None}")


try:
    asyncio.run(asyncio.wait_for(main(), timeout=15))
except asyncio.TimeoutError:
    print("main did not finish within 15s")

Output on 1.28.1 and 2.1.1:

mcp 2.1.1: response status = 500
  logged ERROR by mcp.server.streamable_http: Error handling POST request | exc: ClientDisconnect

(The script caps handle_request at 5 s because the stub send / receive is not a real ASGI server; the 500 and the log line are sent before that.)

Expected: ClientDisconnect caught before the generic handler, logged at debug or info, and no response attempted, since there is nobody to answer.

Python & MCP Python SDK

Python 3.12.13, mcp 1.28.1 and 2.1.1, starlette 1.3.1, macOS

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Commencez dans mcp/server/streamable_http.py, au niveau de StreamableHTTPServerTransport._handle_post_request, en particulier sur la ligne body = await request.body() et le gestionnaire d’exceptions générique qui suit. Comparez la gestion existante des déconnexions dans _handle_message, mentionnée dans #2064 et #2257. Le travail est considéré comme terminé lorsque ClientDisconnect est géré sans trace de pile d’erreur ni réponse 500, et qu’une couverture de régression vérifie ce comportement.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
python
Domaine
api, backend
Type d'issue
Bug
Difficulté
2/5
Temps estimé
1-3 heures
Activité
Active
Clarté
Clairement spécifiée
Accessibilité débutants
78/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.