@@ -62,6 +62,21 @@ class ResumptionError(StreamableHTTPError):
6262 """Raised when resumption request is invalid."""
6363
6464
65+ def _unwrap_exception (exc : BaseException ) -> BaseException :
66+ """Recursively find and return the first StreamableHTTPError in an exception's tree/group."""
67+ if isinstance (exc , StreamableHTTPError ):
68+ return exc
69+
70+ exceptions = getattr (exc , "exceptions" , None )
71+ if exceptions is not None :
72+ for sub_exc in exceptions :
73+ unwrapped = _unwrap_exception (sub_exc )
74+ if isinstance (unwrapped , StreamableHTTPError ):
75+ return unwrapped
76+ return exc
77+
78+
79+
6580@dataclass
6681class RequestContext :
6782 """Context for a request operation."""
@@ -228,11 +243,12 @@ async def handle_get_stream(self, client: httpx2.AsyncClient, read_stream_writer
228243 # Stream ended normally (server closed) - reset attempt counter
229244 attempt = 0
230245
231- except Exception as exc :
246+ except httpx2 . HTTPError as exc :
232247 logger .debug ("GET stream error" , exc_info = True )
233248 attempt += 1
234249 last_exc = exc
235250
251+
236252 if attempt >= MAX_RECONNECTION_ATTEMPTS :
237253 logger .debug (f"GET stream max reconnection attempts ({ MAX_RECONNECTION_ATTEMPTS } ) exceeded" )
238254 raise StreamableHTTPError (
@@ -448,9 +464,10 @@ async def _handle_sse_response(
448464 if is_complete :
449465 await response .aclose ()
450466 return # Normal completion, no reconnect needed
451- except Exception :
467+ except httpx2 . HTTPError :
452468 logger .debug ("SSE stream ended" , exc_info = True ) # pragma: lax no cover
453469
470+
454471 # Stream ended without response - reconnect if we received an event with ID
455472 if last_event_id is not None :
456473 logger .info ("SSE stream disconnected, reconnecting..." )
@@ -532,11 +549,12 @@ async def _handle_reconnection(
532549 # Stream ended again without response - reconnect again (reset attempt counter)
533550 logger .info ("SSE stream disconnected, reconnecting..." )
534551 await self ._handle_reconnection (ctx , reconnect_last_event_id , reconnect_retry_ms , 0 )
535- except Exception as e : # pragma: no cover
552+ except httpx2 . HTTPError as e : # pragma: no cover
536553 logger .debug (f"Reconnection failed: { e } " )
537554 # Try to reconnect again if we still have an event ID
538555 await self ._handle_reconnection (ctx , last_event_id , retry_interval_ms , attempt + 1 )
539556
557+
540558 async def post_writer (
541559 self ,
542560 client : httpx2 .AsyncClient ,
@@ -684,39 +702,46 @@ async def streamable_http_client(
684702
685703 logger .debug (f"Connecting to StreamableHTTP endpoint: { url } " )
686704
687- async with contextlib .AsyncExitStack () as stack :
688- # Only manage client lifecycle if we created it
689- if not client_provided :
690- await stack .enter_async_context (client )
691-
692- read_stream_writer , read_stream = create_context_streams [SessionMessage | Exception ](0 )
693- write_stream , write_stream_reader = create_context_streams [SessionMessage ](0 )
705+ try :
706+ async with contextlib .AsyncExitStack () as stack :
707+ # Only manage client lifecycle if we created it
708+ if not client_provided :
709+ await stack .enter_async_context (client )
694710
695- async with (
696- read_stream_writer ,
697- read_stream ,
698- write_stream ,
699- write_stream_reader ,
700- anyio .create_task_group () as tg ,
701- ):
711+ read_stream_writer , read_stream = create_context_streams [SessionMessage | Exception ](0 )
712+ write_stream , write_stream_reader = create_context_streams [SessionMessage ](0 )
702713
703- def start_get_stream () -> None :
704- tg .start_soon (transport .handle_get_stream , client , read_stream_writer )
705-
706- tg .start_soon (
707- transport .post_writer ,
708- client ,
709- write_stream_reader ,
714+ async with (
710715 read_stream_writer ,
716+ read_stream ,
711717 write_stream ,
712- start_get_stream ,
713- tg ,
714- )
718+ write_stream_reader ,
719+ anyio .create_task_group () as tg ,
720+ ):
721+
722+ def start_get_stream () -> None :
723+ tg .start_soon (transport .handle_get_stream , client , read_stream_writer )
724+
725+ tg .start_soon (
726+ transport .post_writer ,
727+ client ,
728+ write_stream_reader ,
729+ read_stream_writer ,
730+ write_stream ,
731+ start_get_stream ,
732+ tg ,
733+ )
734+
735+ try :
736+ yield read_stream , write_stream
737+ finally :
738+ if transport .session_id and terminate_on_close :
739+ await transport .terminate_session (client )
740+ tg .cancel_scope .cancel ()
741+ await resync_tracer ()
742+ except BaseException as exc :
743+ unwrapped = _unwrap_exception (exc )
744+ if isinstance (unwrapped , StreamableHTTPError ):
745+ raise unwrapped from exc
746+ raise
715747
716- try :
717- yield read_stream , write_stream
718- finally :
719- if transport .session_id and terminate_on_close :
720- await transport .terminate_session (client )
721- tg .cancel_scope .cancel ()
722- await resync_tracer ()
0 commit comments