@@ -422,6 +422,7 @@ async def _handle_sse_response(
422422 assert isinstance (ctx .session_message .message , JSONRPCRequest )
423423 original_request_id = ctx .session_message .message .id
424424
425+ stream_fault : Exception | None = None
425426 try :
426427 event_source = EventSource (response )
427428 async for sse in event_source : # pragma: no branch
@@ -444,19 +445,27 @@ async def _handle_sse_response(
444445 if is_complete :
445446 await response .aclose ()
446447 return # Normal completion, no reconnect needed
447- except Exception :
448+ except Exception as exc :
449+ stream_fault = exc
448450 logger .debug ("SSE stream ended" , exc_info = True ) # pragma: lax no cover
449451
452+ if stream_fault is not None :
453+ # Surface the transport fault to the session's message_handler as an
454+ # Exception item (see IncomingMessage); the waiter itself is resolved
455+ # below with a synthesized error carrying the same exception.
456+ await self ._forward_stream_fault (ctx .read_stream_writer , stream_fault )
457+
450458 # Stream ended without response - reconnect if we received an event with ID
451459 if last_event_id is not None :
452460 logger .info ("SSE stream disconnected, reconnecting..." )
453- await self ._handle_reconnection (ctx , last_event_id , retry_interval_ms )
461+ await self ._handle_reconnection (ctx , last_event_id , retry_interval_ms , last_exc = stream_fault )
454462 else :
455463 # Not resumable: resolve the waiter, else a listen stream's consumer
456464 # would hang forever instead of learning the subscription is lost.
457- await self ._resolve_abandoned_request (
458- ctx .read_stream_writer , original_request_id , "SSE stream ended without a response"
459- )
465+ message = "SSE stream ended without a response"
466+ if stream_fault is not None :
467+ message = f"{ message } : { stream_fault !r} "
468+ await self ._resolve_abandoned_request (ctx .read_stream_writer , original_request_id , message )
460469
461470 async def _resolve_abandoned_request (
462471 self , read_stream_writer : StreamWriter , request_id : RequestId , message : str , * , code : int = CONNECTION_CLOSED
@@ -472,12 +481,27 @@ async def _resolve_abandoned_request(
472481 except (anyio .BrokenResourceError , anyio .ClosedResourceError ):
473482 logger .debug ("read stream closed before request %r could be resolved" , request_id )
474483
484+ async def _forward_stream_fault (self , read_stream_writer : StreamWriter , exc : Exception ) -> None :
485+ """Forward a transport-level fault to the session as an Exception item.
486+
487+ The dispatcher routes Exception items to the session's message_handler via
488+ its on_stream_exception observer (see IncomingMessage); pending request
489+ waiters are resolved separately by the caller with a synthesized error
490+ carrying the same exception.
491+ """
492+ try :
493+ await read_stream_writer .send (exc )
494+ except (anyio .BrokenResourceError , anyio .ClosedResourceError ):
495+ logger .debug ("read stream closed before transport fault %r could be forwarded" , exc )
496+
475497 async def _handle_reconnection (
476498 self ,
477499 ctx : RequestContext ,
478500 last_event_id : str ,
479501 retry_interval_ms : int | None = None ,
480502 attempt : int = 0 ,
503+ * ,
504+ last_exc : Exception | None = None ,
481505 ) -> None :
482506 """Reconnect with Last-Event-ID to resume stream after server disconnect."""
483507 # Only requests reconnect: every caller arrives from a request's response stream.
@@ -488,9 +512,10 @@ async def _handle_reconnection(
488512 # Resolve on give-up: a request with no read timeout (a listen
489513 # stream) would otherwise hang its caller forever.
490514 logger .debug (f"Max reconnection attempts ({ MAX_RECONNECTION_ATTEMPTS } ) exceeded" )
491- await self ._resolve_abandoned_request (
492- ctx .read_stream_writer , original_request_id , "SSE stream ended and reconnection attempts were exhausted"
493- )
515+ message = "SSE stream ended and reconnection attempts were exhausted"
516+ if last_exc is not None :
517+ message = f"{ message } : { last_exc !r} "
518+ await self ._resolve_abandoned_request (ctx .read_stream_writer , original_request_id , message )
494519 return
495520
496521 # Always wait - use server value or default
@@ -527,11 +552,11 @@ async def _handle_reconnection(
527552
528553 # Stream ended again without response - reconnect again (reset attempt counter)
529554 logger .info ("SSE stream disconnected, reconnecting..." )
530- await self ._handle_reconnection (ctx , reconnect_last_event_id , reconnect_retry_ms , 0 )
555+ await self ._handle_reconnection (ctx , reconnect_last_event_id , reconnect_retry_ms , 0 , last_exc = last_exc )
531556 except Exception as e : # pragma: no cover
532557 logger .debug (f"Reconnection failed: { e } " )
533558 # Try to reconnect again if we still have an event ID
534- await self ._handle_reconnection (ctx , last_event_id , retry_interval_ms , attempt + 1 )
559+ await self ._handle_reconnection (ctx , last_event_id , retry_interval_ms , attempt + 1 , last_exc = e )
535560
536561 async def post_writer (
537562 self ,
0 commit comments