|
12 | 12 | from contextlib import asynccontextmanager |
13 | 13 | from dataclasses import dataclass, field |
14 | 14 | from typing import Any |
15 | | -from unittest.mock import MagicMock |
| 15 | +from unittest.mock import AsyncMock, MagicMock, patch |
16 | 16 | from urllib.parse import urlparse |
17 | 17 |
|
18 | 18 | import anyio |
|
45 | 45 | from mcp import MCPError |
46 | 46 | from mcp.client import ClientRequestContext |
47 | 47 | from mcp.client.session import ClientSession |
48 | | -from mcp.client.streamable_http import StreamableHTTPTransport, streamable_http_client |
| 48 | +from mcp.client.streamable_http import ( |
| 49 | + StreamableHTTPError, |
| 50 | + StreamableHTTPTransport, |
| 51 | + streamable_http_client, |
| 52 | +) |
49 | 53 | from mcp.server import Server, ServerRequestContext |
50 | 54 | from mcp.server.streamable_http import ( |
51 | 55 | GET_STREAM_KEY, |
@@ -2283,3 +2287,32 @@ async def asgi_receive() -> Message: |
2283 | 2287 | assert body_chunks[-1] == {"type": "http.response.body", "body": b"", "more_body": False} |
2284 | 2288 | assert "Error in standalone SSE writer" not in caplog.text |
2285 | 2289 | assert "Error in standalone SSE response" not in caplog.text |
| 2290 | + |
| 2291 | + |
| 2292 | +@pytest.mark.anyio |
| 2293 | +async def test_reconnect_failure_propagates_error() -> None: |
| 2294 | + """Client should raise StreamableHTTPError when reconnection fails completely.""" |
| 2295 | + transport = StreamableHTTPTransport(url="http://localhost:8000/mcp") |
| 2296 | + transport.session_id = "test-session" |
| 2297 | + client = AsyncMock(spec=httpx2.AsyncClient) |
| 2298 | + |
| 2299 | + # Create a context-aware stream writer (matches StreamWriter type alias) |
| 2300 | + write_stream, read_stream = create_context_streams[SessionMessage | Exception](1) |
| 2301 | + |
| 2302 | + # Mock client.sse to raise an exception |
| 2303 | + client.sse.side_effect = Exception("Connection refused") |
| 2304 | + |
| 2305 | + # Patch anyio.sleep to avoid waiting |
| 2306 | + with patch("mcp.client.streamable_http.anyio.sleep", new_callable=AsyncMock) as mock_sleep: |
| 2307 | + with pytest.raises(StreamableHTTPError) as exc_info: |
| 2308 | + await transport.handle_get_stream(client, write_stream) |
| 2309 | + assert "Failed to connect to GET stream" in str(exc_info.value) |
| 2310 | + # Should have attempted MAX_RECONNECTION_ATTEMPTS times (default is 2) |
| 2311 | + assert client.sse.call_count == 2 |
| 2312 | + # Should have slept between attempts (attempts - 1 times) |
| 2313 | + assert mock_sleep.call_count == 1 |
| 2314 | + # Verify it slept with the default delay (1000ms / 1000.0 = 1.0s) |
| 2315 | + mock_sleep.assert_called_once_with(1.0) |
| 2316 | + |
| 2317 | + await write_stream.aclose() |
| 2318 | + await read_stream.aclose() |
0 commit comments