@@ -316,6 +316,53 @@ async def caller() -> None:
316316 s .close ()
317317
318318
319+ @pytest .mark .anyio
320+ @pytest .mark .parametrize ("anyio_backend" , ["asyncio" , "trio" ], indirect = True )
321+ async def test_send_raw_request_raises_transport_exception_yielded_mid_await ():
322+ """A blocked send_raw_request is woken with the transport's own exception, not parked
323+ until its timeout elapses; the dispatcher keeps serving once the stream recovers (#1401)."""
324+ c2s_send , c2s_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](32 )
325+ s2c_send , s2c_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](32 )
326+ client : JSONRPCDispatcher [TransportContext ] = JSONRPCDispatcher (s2c_recv , c2s_send )
327+ server : JSONRPCDispatcher [TransportContext ] = JSONRPCDispatcher (c2s_recv , s2c_send )
328+ release_first = anyio .Event ()
329+
330+ async def server_on_request (ctx : DCtx , method : str , params : Mapping [str , Any ] | None ) -> dict [str , Any ]:
331+ # Park the first request so the caller is mid-await when the fault lands.
332+ await release_first .wait ()
333+ return {"echoed" : method , "params" : {}}
334+
335+ async def on_notify (ctx : DCtx , method : str , params : Mapping [str , Any ] | None ) -> None :
336+ raise NotImplementedError
337+
338+ fault_consumed = anyio .Event ()
339+
340+ async def caller () -> None :
341+ with pytest .raises (RuntimeError , match = "transport fault" ):
342+ await client .send_raw_request ("ping" , None )
343+ fault_consumed .set ()
344+
345+ try :
346+ async with anyio .create_task_group () as tg :
347+ await tg .start (client .run , * echo_handlers (Recorder ()))
348+ await tg .start (server .run , server_on_request , on_notify )
349+
350+ tg .start_soon (caller )
351+ await anyio .sleep (0 )
352+ # Fault the client's read side mid-await. The buffered send yields no
353+ # checkpoint, so wait for the waiter to consume the fault first.
354+ await s2c_send .send (RuntimeError ("transport fault" ))
355+ await fault_consumed .wait ()
356+ release_first .set () # the parked first response arrives late and is dropped
357+ # The stream stays open, so a later round-trip must still work.
358+ assert await client .send_raw_request ("ping" , None ) == {"echoed" : "ping" , "params" : {}}
359+ s2c_send .close () # EOF both read streams so run() loops exit and the tg joins
360+ c2s_send .close ()
361+ finally :
362+ for s in (c2s_send , c2s_recv , s2c_send , s2c_recv ):
363+ s .close ()
364+
365+
319366@pytest .mark .anyio
320367async def test_run_returns_cleanly_when_read_stream_receive_end_is_closed ():
321368 """Iterating a closed receive end is EOF, not a crash (stateless SHTTP closes it during teardown)."""
@@ -1826,7 +1873,7 @@ def test_resolve_pending_drops_outcome_when_waiter_stream_already_closed():
18261873 c2s_send , c2s_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](1 )
18271874 s2c_send , s2c_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](1 )
18281875 d : JSONRPCDispatcher [TransportContext ] = JSONRPCDispatcher (s2c_recv , c2s_send )
1829- send , recv = anyio .create_memory_object_stream [dict [str , Any ] | ErrorData ](1 )
1876+ send , recv = anyio .create_memory_object_stream [dict [str , Any ] | ErrorData | Exception ](1 )
18301877 d ._pending [1 ] = _Pending (send = send , receive = recv ) # pyright: ignore[reportPrivateUsage]
18311878 recv .close () # waiter gone - send_nowait will raise BrokenResourceError
18321879 d ._resolve_pending (1 , {"late" : True }) # pyright: ignore[reportPrivateUsage]
@@ -1839,7 +1886,7 @@ def test_fan_out_closed_drops_signal_when_waiter_already_has_outcome():
18391886 c2s_send , c2s_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](1 )
18401887 s2c_send , s2c_recv = anyio .create_memory_object_stream [SessionMessage | Exception ](1 )
18411888 d : JSONRPCDispatcher [TransportContext ] = JSONRPCDispatcher (s2c_recv , c2s_send )
1842- send , recv = anyio .create_memory_object_stream [dict [str , Any ] | ErrorData ](1 )
1889+ send , recv = anyio .create_memory_object_stream [dict [str , Any ] | ErrorData | Exception ](1 )
18431890 d ._pending [1 ] = _Pending (send = send , receive = recv ) # pyright: ignore[reportPrivateUsage]
18441891 send .send_nowait ({"real" : "result" })
18451892 d ._fan_out_closed () # pyright: ignore[reportPrivateUsage]
0 commit comments