4141logger = logging .getLogger (__name__ )
4242
4343
44+ def _parse_rest_error (
45+ error_payload : dict [str , Any ],
46+ fallback_message : str ,
47+ ) -> Exception | None :
48+ """Parses a REST error payload and returns the appropriate A2AError.
49+
50+ Args:
51+ error_payload: The parsed JSON error payload.
52+ fallback_message: Message to use if the payload has no ``message``.
53+
54+ Returns:
55+ The mapped A2AError if a known reason was found, otherwise ``None``.
56+ """
57+ error_data = error_payload .get ('error' , {})
58+ message = error_data .get ('message' , fallback_message )
59+ details = error_data .get ('details' , [])
60+ if not isinstance (details , list ):
61+ return None
62+
63+ # The `details` array can contain multiple different error objects.
64+ # We extract the first `ErrorInfo` object because it contains the
65+ # specific `reason` code needed to map this back to a Python A2AError.
66+ for d in details :
67+ if (
68+ isinstance (d , dict )
69+ and d .get ('@type' ) == 'type.googleapis.com/google.rpc.ErrorInfo'
70+ ):
71+ reason = d .get ('reason' )
72+ metadata = d .get ('metadata' ) or {}
73+ if isinstance (reason , str ):
74+ exception_cls = A2A_REASON_TO_ERROR .get (reason )
75+ if exception_cls :
76+ exc = exception_cls (message )
77+ if metadata :
78+ exc .data = metadata
79+ return exc
80+ break
81+
82+ return None
83+
84+
4485@trace_class (kind = SpanKind .CLIENT )
4586class RestTransport (ClientTransport ):
4687 """A REST transport for the A2A client."""
@@ -294,39 +335,12 @@ def _handle_http_error(self, e: httpx.HTTPStatusError) -> NoReturn:
294335 """Handles HTTP status errors and raises the appropriate A2AError."""
295336 try :
296337 error_payload = e .response .json ()
297- error_data = error_payload .get ('error' , {})
298-
299- message = error_data .get ('message' , str (e ))
300- details = error_data .get ('details' , [])
301- if not isinstance (details , list ):
302- details = []
303-
304- # The `details` array can contain multiple different error objects.
305- # We extract the first `ErrorInfo` object because it contains the
306- # specific `reason` code needed to map this back to a Python A2AError.
307- error_info = {}
308- for d in details :
309- if (
310- isinstance (d , dict )
311- and d .get ('@type' )
312- == 'type.googleapis.com/google.rpc.ErrorInfo'
313- ):
314- error_info = d
315- break
316- reason = error_info .get ('reason' )
317- metadata = error_info .get ('metadata' ) or {}
318-
319- if isinstance (reason , str ):
320- exception_cls = A2A_REASON_TO_ERROR .get (reason )
321- if exception_cls :
322- exc = exception_cls (message )
323- if metadata :
324- exc .data = metadata
325- raise exc from e
338+ mapped = _parse_rest_error (error_payload , str (e ))
339+ if mapped :
340+ raise mapped from e
326341 except (json .JSONDecodeError , ValueError ):
327342 pass
328343
329- # Fallback mappings for status codes if 'type' is missing or unknown
330344 status_code = e .response .status_code
331345 if status_code == httpx .codes .NOT_FOUND :
332346 raise MethodNotFoundError (
@@ -335,6 +349,14 @@ def _handle_http_error(self, e: httpx.HTTPStatusError) -> NoReturn:
335349
336350 raise A2AClientError (f'HTTP Error { status_code } : { e } ' ) from e
337351
352+ def _handle_sse_error (self , sse_data : str ) -> NoReturn :
353+ """Handles SSE error events by parsing the REST error payload and raising the appropriate A2AError."""
354+ error_payload = json .loads (sse_data )
355+ mapped = _parse_rest_error (error_payload , sse_data )
356+ if mapped :
357+ raise mapped
358+ raise A2AClientError (sse_data )
359+
338360 async def _send_stream_request (
339361 self ,
340362 method : str ,
@@ -352,6 +374,7 @@ async def _send_stream_request(
352374 method ,
353375 f'{ self .url } { path } ' ,
354376 self ._handle_http_error ,
377+ self ._handle_sse_error ,
355378 json = json ,
356379 ** http_kwargs ,
357380 ):
0 commit comments