@@ -347,6 +347,10 @@ async def run(
347347 # the initialization lifecycle, but can do so with any available node
348348 # rather than requiring initialization for each connection.
349349 stateless : bool = False ,
350+ # When True, treat read EOF as a half-close and allow in-flight handlers
351+ # to drain their responses via the still-open write stream (e.g. stdio
352+ # with bash-redirected stdin).
353+ drain_on_read_close : bool = False ,
350354 ):
351355 async with AsyncExitStack () as stack :
352356 lifespan_context = await stack .enter_async_context (self .lifespan (self ))
@@ -356,26 +360,35 @@ async def run(
356360 write_stream ,
357361 initialization_options ,
358362 stateless = stateless ,
363+ close_write_stream_on_read_close = not drain_on_read_close ,
359364 )
360365 )
361366
362367 async with anyio .create_task_group () as tg :
363- async for message in session .incoming_messages :
364- logger .debug ("Received message: %s" , message )
365-
366- if isinstance (message , RequestResponder ) and message .context is not None :
367- context = message .context
368- else :
369- context = contextvars .copy_context ()
370-
371- context .run (
372- tg .start_soon ,
373- self ._handle_message ,
374- message ,
375- session ,
376- lifespan_context ,
377- raise_exceptions ,
378- )
368+ try :
369+ async for message in session .incoming_messages :
370+ logger .debug ("Received message: %s" , message )
371+
372+ if isinstance (message , RequestResponder ) and message .context is not None :
373+ context = message .context
374+ else :
375+ context = contextvars .copy_context ()
376+
377+ context .run (
378+ tg .start_soon ,
379+ self ._handle_message ,
380+ message ,
381+ session ,
382+ lifespan_context ,
383+ raise_exceptions ,
384+ )
385+ finally :
386+ if not drain_on_read_close :
387+ # Transport closed: cancel in-flight handlers. Without this the
388+ # TG join waits for them, and when they eventually try to
389+ # respond they hit a closed write stream (the session's
390+ # _receive_loop closed it when the read stream ended).
391+ tg .cancel_scope .cancel ()
379392
380393 async def _handle_message (
381394 self ,
0 commit comments