@@ -1878,6 +1878,40 @@ async def fail_reconnect(*args: Any, **kwargs: Any) -> None: # pragma: no cover
18781878 await read_stream .aclose ()
18791879
18801880
1881+ @pytest .mark .anyio
1882+ async def test_sse_response_does_not_reconnect_after_terminal_then_drain_error (monkeypatch : pytest .MonkeyPatch ):
1883+ transport = StreamableHTTPTransport (url = "http://localhost:8000/mcp" )
1884+ response = _FakeStreamResponse ()
1885+
1886+ class FakeEventSource :
1887+ def __init__ (self , response : _FakeStreamResponse ) -> None :
1888+ self .response = response
1889+
1890+ async def aiter_sse (self ):
1891+ yield _response_sse (1 )
1892+ raise RuntimeError ("drain failed after terminal response" )
1893+
1894+ async def fail_reconnect (* args : Any , ** kwargs : Any ) -> None : # pragma: no cover
1895+ raise AssertionError ("completed responses should not reconnect after drain errors" )
1896+
1897+ monkeypatch .setattr (streamable_http_module , "EventSource" , FakeEventSource )
1898+ monkeypatch .setattr (transport , "_handle_reconnection" , fail_reconnect )
1899+
1900+ write_stream , read_stream = create_context_streams [SessionMessage | Exception ](2 )
1901+ async with httpx .AsyncClient () as client :
1902+ try :
1903+ ctx = _make_streamable_http_request_context (1 , client , write_stream )
1904+ await transport ._handle_sse_response (response , ctx )
1905+
1906+ message = await read_stream .receive ()
1907+ assert isinstance (message , SessionMessage )
1908+ assert isinstance (message .message , types .JSONRPCResponse )
1909+ assert message .message .id == 1
1910+ finally :
1911+ await write_stream .aclose ()
1912+ await read_stream .aclose ()
1913+
1914+
18811915@pytest .mark .anyio
18821916async def test_reconnection_drains_after_terminal_response (monkeypatch : pytest .MonkeyPatch ):
18831917 """Resumed GET responses use EOF draining instead of response.aclose()."""
@@ -1914,6 +1948,44 @@ async def fake_aconnect_sse(*args: Any, **kwargs: Any):
19141948 await read_stream .aclose ()
19151949
19161950
1951+ @pytest .mark .anyio
1952+ async def test_reconnection_does_not_retry_after_terminal_then_drain_error (monkeypatch : pytest .MonkeyPatch ):
1953+ transport = StreamableHTTPTransport (url = "http://localhost:8000/mcp" )
1954+ response = _FakeStreamResponse ()
1955+ attempts = 0
1956+
1957+ class FakeReconnectionEventSource :
1958+ def __init__ (self , response : _FakeStreamResponse ) -> None :
1959+ self .response = response
1960+
1961+ async def aiter_sse (self ):
1962+ yield _response_sse ("abc" )
1963+ raise RuntimeError ("drain failed after terminal response" )
1964+
1965+ @asynccontextmanager
1966+ async def fake_aconnect_sse (* args : Any , ** kwargs : Any ):
1967+ nonlocal attempts
1968+ attempts += 1
1969+ yield FakeReconnectionEventSource (response )
1970+
1971+ monkeypatch .setattr (streamable_http_module , "aconnect_sse" , fake_aconnect_sse )
1972+
1973+ write_stream , read_stream = create_context_streams [SessionMessage | Exception ](2 )
1974+ async with httpx .AsyncClient () as client :
1975+ try :
1976+ ctx = _make_streamable_http_request_context ("abc" , client , write_stream )
1977+ await transport ._handle_reconnection (ctx , "previous-event" , retry_interval_ms = 0 )
1978+
1979+ assert attempts == 1
1980+ message = await read_stream .receive ()
1981+ assert isinstance (message , SessionMessage )
1982+ assert isinstance (message .message , types .JSONRPCResponse )
1983+ assert message .message .id == "abc"
1984+ finally :
1985+ await write_stream .aclose ()
1986+ await read_stream .aclose ()
1987+
1988+
19171989@pytest .mark .anyio
19181990async def test_reconnection_retries_after_failed_resume (monkeypatch : pytest .MonkeyPatch ):
19191991 """A failed resume attempt falls back to the next reconnection attempt."""
0 commit comments