@@ -1594,6 +1594,48 @@ async def test_transport_exception_in_read_stream_is_logged_and_dropped():
15941594 s .close ()
15951595
15961596
1597+ @pytest .mark .anyio
1598+ async def test_send_raw_request_raises_connection_closed_on_transport_exception ():
1599+ """A blocked send_raw_request is woken with CONNECTION_CLOSED when a transport
1600+ Exception item arrives; the receive loop stays healthy for a later request."""
1601+ c2s_send , c2s_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](32 )
1602+ s2c_send , s2c_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](32 )
1603+ client : JSONRPCDispatcher [TransportContext ] = JSONRPCDispatcher (s2c_recv , c2s_send )
1604+ on_request , on_notify = echo_handlers (Recorder ())
1605+ hiccup = ValueError ("transport hiccup" )
1606+ try :
1607+ async with anyio .create_task_group () as tg :
1608+ await tg .start (client .run , on_request , on_notify )
1609+
1610+ async def caller () -> None :
1611+ with pytest .raises (MCPError ) as exc :
1612+ await client .send_raw_request ("ping" , None )
1613+ assert exc .value .error .code == CONNECTION_CLOSED
1614+ assert "Transport error:" in exc .value .error .message
1615+ assert "transport hiccup" in exc .value .error .message
1616+
1617+ tg .start_soon (caller )
1618+ # Let the outbound request register in `_pending` before the fault.
1619+ req = await c2s_recv .receive ()
1620+ assert isinstance (req , SessionMessage )
1621+ assert isinstance (req .message , JSONRPCRequest )
1622+ await s2c_send .send (hiccup )
1623+ await anyio .sleep (0 )
1624+ # Loop must still serve after the Exception item (not closed like EOF).
1625+ await s2c_send .send (
1626+ SessionMessage (message = JSONRPCRequest (jsonrpc = "2.0" , id = 99 , method = "t" , params = None ))
1627+ )
1628+ with anyio .fail_after (5 ):
1629+ resp = await c2s_recv .receive ()
1630+ assert isinstance (resp , SessionMessage )
1631+ assert isinstance (resp .message , JSONRPCResponse )
1632+ assert resp .message .id == 99
1633+ tg .cancel_scope .cancel ()
1634+ finally :
1635+ for s in (c2s_send , c2s_recv , s2c_send , s2c_recv ):
1636+ s .close ()
1637+
1638+
15971639@pytest .mark .anyio
15981640async def test_on_stream_exception_observes_transport_exceptions ():
15991641 """With an observer set, Exception items reach it instead of being dropped; the loop stays healthy."""
0 commit comments