Skip to content

Commit 601e4ac

Browse files
committed
Handle client disconnects while reading POST body
1 parent d2290ca commit 601e4ac

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

src/mcp/server/streamable_http.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from mcp_types.version import is_version_at_least
3838
from pydantic import ValidationError
3939
from sse_starlette import EventSourceResponse
40-
from starlette.requests import Request
40+
from starlette.requests import ClientDisconnect, Request
4141
from starlette.responses import Response
4242
from starlette.types import Receive, Scope, Send
4343

@@ -714,6 +714,9 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re
714714
finally:
715715
await sse_stream_reader.aclose()
716716

717+
except ClientDisconnect:
718+
logger.debug("Client disconnected while reading POST request body")
719+
return
717720
except Exception as err:
718721
logger.exception("Error handling POST request")
719722
response = self._create_error_response(

tests/shared/test_streamable_http.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,46 @@ async def send(message: Message) -> None:
656656
return next(message["status"] for message in sent if message["type"] == "http.response.start")
657657

658658

659+
@pytest.mark.anyio
660+
async def test_streamable_http_post_client_disconnect_is_not_reported_as_500(
661+
caplog: pytest.LogCaptureFixture,
662+
) -> None:
663+
caplog.set_level(logging.ERROR, logger="mcp.server.streamable_http")
664+
transport = StreamableHTTPServerTransport(mcp_session_id="valid-id")
665+
assert transport.mcp_session_id is not None
666+
scope: Scope = {
667+
"type": "http",
668+
"method": "POST",
669+
"path": "/mcp",
670+
"query_string": b"",
671+
"headers": [
672+
(b"content-type", b"application/json"),
673+
(b"accept", b"application/json, text/event-stream"),
674+
(MCP_SESSION_ID_HEADER.encode(), transport.mcp_session_id.encode()),
675+
],
676+
}
677+
sent: list[Message] = []
678+
679+
async def receive() -> Message:
680+
return {"type": "http.disconnect"}
681+
682+
async def send(message: Message) -> None:
683+
sent.append(message)
684+
685+
async with transport.connect():
686+
with anyio.move_on_after(1) as cancel_scope:
687+
await transport.handle_request(scope, receive, send)
688+
689+
assert not cancel_scope.cancel_called
690+
assert not sent
691+
assert [
692+
record.getMessage()
693+
for record in caplog.records
694+
if record.name == "mcp.server.streamable_http" and record.levelno >= logging.ERROR
695+
] == []
696+
await transport.terminate()
697+
698+
659699
@pytest.mark.anyio
660700
async def test_transport_whose_idle_period_ran_out_answers_as_terminated() -> None:
661701
"""Once the idle scope has fired, a request that still reaches the transport is answered 404 and the

0 commit comments

Comments
 (0)