modelcontextprotocol / modelcontextprotocol/python-sdk
StreamableHTTP: a client disconnect while reading the POST body is logged as an exception and answered 500
還沒有人認領這個 Issue。
- 主要語言
- Python
- 星號
- 24.3k
- 分支
- 4k
- 平均合併
- 1 天 1 小時
- 30 天內合併 PR
- 31
描述
Initial Checks
- I confirm that I'm using the newest release of my line (the latest 2.x, or the latest 1.x if I'm still on v1)
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
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
貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
研究方向
從 mcp/server/streamable_http.py 中的 StreamableHTTPServerTransport._handle_post_request 開始,特別查看 body = await request.body() 這一行以及後面的通用例外處理器。比較 _handle_message 中現有的中斷連線處理方式,#2064 和 #2257 有參照到這項處理。完成標準是:ClientDisconnect 在處理時不會產生錯誤堆疊追蹤或 500 回應,且回歸測試涵蓋範圍驗證了此行為。
由索引模型根據 Issue 內容生成。
評估
- 技術堆疊
- python
- 領域
- api, backend
- Issue 類型
- 缺陷
- 難度
- 2/5
- 預估耗時
- 1-3 小時
- 活躍度
- 活躍
- 描述清晰度
- 描述清楚
- 新手友好度
- 78/100