@@ -371,6 +371,10 @@ async def run(
371371 # the initialization lifecycle, but can do so with any available node
372372 # rather than requiring initialization for each connection.
373373 stateless : bool = False ,
374+ # When True, treat read EOF as a half-close and allow in-flight handlers
375+ # to drain their responses via the still-open write stream (e.g. stdio
376+ # with bash-redirected stdin).
377+ drain_on_read_close : bool = False ,
374378 ):
375379 async with AsyncExitStack () as stack :
376380 lifespan_context = await stack .enter_async_context (self .lifespan (self ))
@@ -380,6 +384,7 @@ async def run(
380384 write_stream ,
381385 initialization_options ,
382386 stateless = stateless ,
387+ close_write_stream_on_read_close = not drain_on_read_close ,
383388 )
384389 )
385390
@@ -390,22 +395,30 @@ async def run(
390395 await stack .enter_async_context (task_support .run ())
391396
392397 async with anyio .create_task_group () as tg :
393- async for message in session .incoming_messages :
394- logger .debug ("Received message: %s" , message )
395-
396- if isinstance (message , RequestResponder ) and message .context is not None :
397- context = message .context
398- else :
399- context = contextvars .copy_context ()
400-
401- context .run (
402- tg .start_soon ,
403- self ._handle_message ,
404- message ,
405- session ,
406- lifespan_context ,
407- raise_exceptions ,
408- )
398+ try :
399+ async for message in session .incoming_messages :
400+ logger .debug ("Received message: %s" , message )
401+
402+ if isinstance (message , RequestResponder ) and message .context is not None :
403+ context = message .context
404+ else :
405+ context = contextvars .copy_context ()
406+
407+ context .run (
408+ tg .start_soon ,
409+ self ._handle_message ,
410+ message ,
411+ session ,
412+ lifespan_context ,
413+ raise_exceptions ,
414+ )
415+ finally :
416+ if not drain_on_read_close :
417+ # Transport closed: cancel in-flight handlers. Without this the
418+ # TG join waits for them, and when they eventually try to
419+ # respond they hit a closed write stream (the session's
420+ # _receive_loop closed it when the read stream ended).
421+ tg .cancel_scope .cancel ()
409422
410423 async def _handle_message (
411424 self ,
0 commit comments