@@ -912,6 +912,63 @@ async def test_transport_exception_in_read_stream_is_logged_and_dropped():
912912 s .close ()
913913
914914
915+ @pytest .mark .anyio
916+ async def test_on_stream_exception_observes_transport_exceptions ():
917+ """With an observer set, Exception items reach it instead of being dropped; the loop stays healthy."""
918+ c2s_send , c2s_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](4 )
919+ s2c_send , s2c_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](4 )
920+
921+ seen : list [Exception ] = []
922+
923+ async def observe (exc : Exception ) -> None :
924+ seen .append (exc )
925+
926+ server : JSONRPCDispatcher [TransportContext ] = JSONRPCDispatcher (c2s_recv , s2c_send , on_stream_exception = observe )
927+ on_request , on_notify = echo_handlers (Recorder ())
928+ hiccup = ValueError ("transport hiccup" )
929+ try :
930+ async with anyio .create_task_group () as tg :
931+ await tg .start (server .run , on_request , on_notify )
932+ await c2s_send .send (hiccup )
933+ await c2s_send .send (SessionMessage (message = JSONRPCRequest (jsonrpc = "2.0" , id = 1 , method = "t" , params = None )))
934+ with anyio .fail_after (5 ):
935+ resp = await s2c_recv .receive ()
936+ assert isinstance (resp , SessionMessage )
937+ assert isinstance (resp .message , JSONRPCResponse )
938+ tg .cancel_scope .cancel ()
939+ finally :
940+ for s in (c2s_send , c2s_recv , s2c_send , s2c_recv ):
941+ s .close ()
942+ assert seen == [hiccup ]
943+
944+
945+ @pytest .mark .anyio
946+ async def test_on_stream_exception_observer_raising_is_contained (caplog : pytest .LogCaptureFixture ):
947+ """A raising observer costs the item, not the connection: it runs in the read loop itself."""
948+ c2s_send , c2s_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](4 )
949+ s2c_send , s2c_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](4 )
950+
951+ async def observe (exc : Exception ) -> None :
952+ raise RuntimeError ("observer boom" )
953+
954+ server : JSONRPCDispatcher [TransportContext ] = JSONRPCDispatcher (c2s_recv , s2c_send , on_stream_exception = observe )
955+ on_request , on_notify = echo_handlers (Recorder ())
956+ try :
957+ async with anyio .create_task_group () as tg :
958+ await tg .start (server .run , on_request , on_notify )
959+ await c2s_send .send (ValueError ("transport hiccup" ))
960+ await c2s_send .send (SessionMessage (message = JSONRPCRequest (jsonrpc = "2.0" , id = 1 , method = "t" , params = None )))
961+ with anyio .fail_after (5 ):
962+ resp = await s2c_recv .receive ()
963+ assert isinstance (resp , SessionMessage )
964+ assert isinstance (resp .message , JSONRPCResponse )
965+ tg .cancel_scope .cancel ()
966+ finally :
967+ for s in (c2s_send , c2s_recv , s2c_send , s2c_recv ):
968+ s .close ()
969+ assert "on_stream_exception observer raised" in caplog .text
970+
971+
915972@pytest .mark .anyio
916973async def test_progress_notification_for_unknown_token_falls_through_to_on_notify ():
917974 async with running_pair (jsonrpc_pair ) as (client , _server , _crec , srec ):
0 commit comments