Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/mcp/server/streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from mcp_types.version import is_version_at_least
from pydantic import ValidationError
from sse_starlette import EventSourceResponse
from starlette.requests import Request
from starlette.requests import ClientDisconnect, Request
from starlette.responses import Response
from starlette.types import Receive, Scope, Send

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

except ClientDisconnect:
logger.debug("Client disconnected while reading POST request body")
return
except Exception as err:
logger.exception("Error handling POST request")
response = self._create_error_response(
Expand Down
40 changes: 40 additions & 0 deletions tests/shared/test_streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,46 @@ async def send(message: Message) -> None:
return next(message["status"] for message in sent if message["type"] == "http.response.start")


@pytest.mark.anyio
async def test_streamable_http_post_client_disconnect_is_not_reported_as_500(
caplog: pytest.LogCaptureFixture,
) -> None:
caplog.set_level(logging.ERROR, logger="mcp.server.streamable_http")
transport = StreamableHTTPServerTransport(mcp_session_id="valid-id")
assert transport.mcp_session_id is not None
scope: 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"),
(MCP_SESSION_ID_HEADER.encode(), transport.mcp_session_id.encode()),
],
}
sent: list[Message] = []

async def receive() -> Message:
return {"type": "http.disconnect"}

async def send(message: Message) -> None:
sent.append(message)

async with transport.connect():
with anyio.move_on_after(1) as cancel_scope:
await transport.handle_request(scope, receive, send)

assert not cancel_scope.cancel_called
assert not sent
assert [
record.getMessage()
for record in caplog.records
if record.name == "mcp.server.streamable_http" and record.levelno >= logging.ERROR
] == []
await transport.terminate()


@pytest.mark.anyio
async def test_transport_whose_idle_period_ran_out_answers_as_terminated() -> None:
"""Once the idle scope has fired, a request that still reaches the transport is answered 404 and the
Expand Down
Loading