|
1 | 1 | """Regression coverage for the StreamableHTTP per-session response router.""" |
2 | 2 |
|
| 3 | +import logging |
| 4 | + |
3 | 5 | import anyio |
4 | 6 | import pytest |
5 | 7 | from mcp_types import JSONRPCMessage, JSONRPCResponse |
|
14 | 16 | StreamableHTTPServerTransport, |
15 | 17 | StreamId, |
16 | 18 | ) |
| 19 | +from mcp.shared._context_streams import create_context_streams |
17 | 20 | from mcp.shared.message import SessionMessage |
18 | 21 |
|
19 | 22 |
|
@@ -44,6 +47,44 @@ async def send(self, message: Message) -> None: |
44 | 47 | self.sent.append(message) |
45 | 48 |
|
46 | 49 |
|
| 50 | +class _AsgiDisconnect(_AsgiPost): |
| 51 | + """A POST whose body stream disconnects before the declared body is complete.""" |
| 52 | + |
| 53 | + async def receive(self) -> Message: |
| 54 | + if not self._body_sent: |
| 55 | + self._body_sent = True |
| 56 | + return {"type": "http.request", "body": self._body, "more_body": True} |
| 57 | + return {"type": "http.disconnect"} |
| 58 | + |
| 59 | + |
| 60 | +@pytest.mark.anyio |
| 61 | +async def test_post_client_disconnect_is_not_reported_as_server_error(caplog: pytest.LogCaptureFixture) -> None: |
| 62 | + transport = StreamableHTTPServerTransport(mcp_session_id=None) |
| 63 | + post = _AsgiDisconnect( |
| 64 | + b'{"jsonrpc":"2.0",', |
| 65 | + [ |
| 66 | + (b"accept", b"application/json, text/event-stream"), |
| 67 | + (b"content-type", b"application/json"), |
| 68 | + ], |
| 69 | + ) |
| 70 | + read_stream_writer, read_stream = create_context_streams[SessionMessage | Exception](1) |
| 71 | + transport._read_stream_writer = read_stream_writer |
| 72 | + |
| 73 | + try: |
| 74 | + with caplog.at_level(logging.ERROR, logger="mcp.server.streamable_http"): |
| 75 | + await transport.handle_request(post.scope, post.receive, post.send) |
| 76 | + |
| 77 | + await read_stream_writer.aclose() |
| 78 | + with pytest.raises(anyio.EndOfStream): |
| 79 | + await read_stream.receive() |
| 80 | + finally: |
| 81 | + await read_stream_writer.aclose() |
| 82 | + await read_stream.aclose() |
| 83 | + |
| 84 | + assert post.sent == [] |
| 85 | + assert not caplog.records |
| 86 | + |
| 87 | + |
47 | 88 | @pytest.mark.anyio |
48 | 89 | async def test_router_unconsumed_request_stream_does_not_block_siblings() -> None: |
49 | 90 | """A response whose `sse_writer` is not yet receiving must not park the router (#1764). |
|
0 commit comments