Skip to content

Commit aaac66f

Browse files
committed
refactor(client): map error responses to domain errors
Re #737
1 parent e2ef540 commit aaac66f

16 files changed

Lines changed: 271 additions & 546 deletions

src/a2a/client/__init__.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@
1313
from a2a.client.client_factory import ClientFactory, minimal_agent_card
1414
from a2a.client.errors import (
1515
A2AClientError,
16-
A2AClientHTTPError,
17-
A2AClientJSONError,
18-
A2AClientTimeoutError,
16+
AgentCardResolutionError,
1917
)
2018
from a2a.client.helpers import create_text_message_object
2119
from a2a.client.middleware import ClientCallContext, ClientCallInterceptor
@@ -27,9 +25,7 @@
2725
__all__ = [
2826
'A2ACardResolver',
2927
'A2AClientError',
30-
'A2AClientHTTPError',
31-
'A2AClientJSONError',
32-
'A2AClientTimeoutError',
28+
'AgentCardResolutionError',
3329
'AuthInterceptor',
3430
'BaseClient',
3531
'Client',

src/a2a/client/card_resolver.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@
88

99
from google.protobuf.json_format import ParseDict, ParseError
1010

11-
from a2a.client.errors import (
12-
A2AClientHTTPError,
13-
A2AClientJSONError,
14-
)
11+
from a2a.client.errors import AgentCardResolutionError
1512
from a2a.types.a2a_pb2 import (
1613
AgentCard,
1714
)
@@ -64,9 +61,9 @@ async def get_agent_card(
6461
An `AgentCard` object representing the agent's capabilities.
6562
6663
Raises:
67-
A2AClientHTTPError: If an HTTP error occurs during the request.
68-
A2AClientJSONError: If the response body cannot be decoded as JSON
69-
or validated against the AgentCard schema.
64+
A2AClientError: If an HTTP error occurs during the request, if the
65+
response body cannot be decoded as JSON, or if it cannot be
66+
validated against the AgentCard schema.
7067
"""
7168
if not relative_card_path:
7269
# Use the default public agent card path configured during initialization
@@ -92,21 +89,19 @@ async def get_agent_card(
9289
if signature_verifier:
9390
signature_verifier(agent_card)
9491
except httpx.HTTPStatusError as e:
95-
raise A2AClientHTTPError(
96-
e.response.status_code,
97-
f'Failed to fetch agent card from {target_url}: {e}',
92+
raise AgentCardResolutionError(
93+
f'Failed to fetch agent card from {target_url} (HTTP {e.response.status_code}): {e}',
9894
) from e
9995
except json.JSONDecodeError as e:
100-
raise A2AClientJSONError(
96+
raise AgentCardResolutionError(
10197
f'Failed to parse JSON for agent card from {target_url}: {e}'
10298
) from e
10399
except httpx.RequestError as e:
104-
raise A2AClientHTTPError(
105-
503,
106-
f'Network communication error fetching agent card from {target_url}: {e}',
100+
raise AgentCardResolutionError(
101+
f'Network communication error fetching agent card from {target_url} (HTTP 503): {e}',
107102
) from e
108103
except ParseError as e:
109-
raise A2AClientJSONError(
104+
raise AgentCardResolutionError(
110105
f'Failed to validate agent card structure from {target_url}: {e}'
111106
) from e
112107

src/a2a/client/client_task_manager.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import logging
22

3-
from a2a.client.errors import (
4-
A2AClientInvalidArgsError,
5-
A2AClientInvalidStateError,
6-
)
3+
from a2a.client.errors import A2AClientError
74
from a2a.types.a2a_pb2 import (
85
Message,
96
StreamResponse,
@@ -53,15 +50,15 @@ def get_task_or_raise(self) -> Task:
5350
The `Task` object.
5451
5552
Raises:
56-
A2AClientInvalidStateError: If there is no current known Task.
53+
A2AClientError: If there is no current known Task.
5754
"""
5855
if not (task := self.get_task()):
5956
# Note: The source of this error is either from bad client usage
6057
# or from the server sending invalid updates. It indicates that this
6158
# task manager has not consumed any information about a task, yet
6259
# the caller is attempting to retrieve the current state of the task
6360
# it expects to be present.
64-
raise A2AClientInvalidStateError('no current Task')
61+
raise A2AClientError('no current Task')
6562
return task
6663

6764
async def process(
@@ -79,7 +76,7 @@ async def process(
7976
The updated `Task` object after processing the event.
8077
8178
Raises:
82-
ClientError: If the task ID in the event conflicts with the TaskManager's ID
79+
A2AClientError: If the task ID in the event conflicts with the TaskManager's ID
8380
when the TaskManager's ID is already set.
8481
"""
8582
if event.HasField('message'):
@@ -88,7 +85,7 @@ async def process(
8885

8986
if event.HasField('task'):
9087
if self._current_task:
91-
raise A2AClientInvalidArgsError(
88+
raise A2AClientError(
9289
'Task is already set, create new manager for new tasks.'
9390
)
9491
await self._save_task(event.task)

src/a2a/client/errors.py

Lines changed: 4 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,11 @@
11
"""Custom exceptions for the A2A client."""
22

3-
from typing import Any
3+
from a2a.utils.errors import A2AError
44

55

6-
class A2AClientError(Exception):
6+
class A2AClientError(A2AError):
77
"""Base exception for A2A Client errors."""
88

99

10-
class A2AClientHTTPError(A2AClientError):
11-
"""Client exception for HTTP errors received from the server."""
12-
13-
def __init__(self, status_code: int, message: str):
14-
"""Initializes the A2AClientHTTPError.
15-
16-
Args:
17-
status_code: The HTTP status code of the response.
18-
message: A descriptive error message.
19-
"""
20-
self.status_code = status_code
21-
self.message = message
22-
super().__init__(f'HTTP Error {status_code}: {message}')
23-
24-
def __repr__(self) -> str:
25-
"""Returns an unambiguous representation showing structured attributes."""
26-
return (
27-
f'{self.__class__.__name__}('
28-
f'status_code={self.status_code!r}, '
29-
f'message={self.message!r})'
30-
)
31-
32-
33-
class A2AClientJSONError(A2AClientError):
34-
"""Client exception for JSON errors during response parsing or validation."""
35-
36-
def __init__(self, message: str):
37-
"""Initializes the A2AClientJSONError.
38-
39-
Args:
40-
message: A descriptive error message.
41-
"""
42-
self.message = message
43-
super().__init__(f'JSON Error: {message}')
44-
45-
def __repr__(self) -> str:
46-
"""Returns an unambiguous representation showing structured attributes."""
47-
return f'{self.__class__.__name__}(message={self.message!r})'
48-
49-
50-
class A2AClientTimeoutError(A2AClientError):
51-
"""Client exception for timeout errors during a request."""
52-
53-
def __init__(self, message: str):
54-
"""Initializes the A2AClientTimeoutError.
55-
56-
Args:
57-
message: A descriptive error message.
58-
"""
59-
self.message = message
60-
super().__init__(f'Timeout Error: {message}')
61-
62-
def __repr__(self) -> str:
63-
"""Returns an unambiguous representation showing structured attributes."""
64-
return f'{self.__class__.__name__}(message={self.message!r})'
65-
66-
67-
class A2AClientInvalidArgsError(A2AClientError):
68-
"""Client exception for invalid arguments passed to a method."""
69-
70-
def __init__(self, message: str):
71-
"""Initializes the A2AClientInvalidArgsError.
72-
73-
Args:
74-
message: A descriptive error message.
75-
"""
76-
self.message = message
77-
super().__init__(f'Invalid arguments error: {message}')
78-
79-
def __repr__(self) -> str:
80-
"""Returns an unambiguous representation showing structured attributes."""
81-
return f'{self.__class__.__name__}(message={self.message!r})'
82-
83-
84-
class A2AClientInvalidStateError(A2AClientError):
85-
"""Client exception for an invalid client state."""
86-
87-
def __init__(self, message: str):
88-
"""Initializes the A2AClientInvalidStateError.
89-
90-
Args:
91-
message: A descriptive error message.
92-
"""
93-
self.message = message
94-
super().__init__(f'Invalid state error: {message}')
95-
96-
def __repr__(self) -> str:
97-
"""Returns an unambiguous representation showing structured attributes."""
98-
return f'{self.__class__.__name__}(message={self.message!r})'
99-
100-
101-
class A2AClientJSONRPCError(A2AClientError):
102-
"""Client exception for JSON-RPC errors returned by the server."""
103-
104-
error: dict[str, Any]
105-
106-
def __init__(self, error: dict[str, Any]):
107-
"""Initializes the A2AClientJsonRPCError.
108-
109-
Args:
110-
error: The JSON-RPC error dict from the jsonrpc library.
111-
"""
112-
self.error = error
113-
super().__init__(f'JSON-RPC Error {self.error}')
10+
class AgentCardResolutionError(A2AClientError):
11+
"""Exception raised when an agent card cannot be resolved."""

src/a2a/client/transports/grpc.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import logging
22

33
from collections.abc import AsyncGenerator, Callable
4+
from functools import wraps
5+
from typing import Any, NoReturn
6+
7+
import a2a.utils.errors
8+
9+
from a2a.client.errors import A2AClientError
410

511

612
try:
@@ -43,6 +49,39 @@
4349
logger = logging.getLogger(__name__)
4450

4551

52+
def _map_grpc_error(e: grpc.aio.AioRpcError) -> NoReturn:
53+
details = e.details()
54+
if isinstance(details, str) and ': ' in details:
55+
error_type_name, error_message = details.split(': ', 1)
56+
exception_cls = getattr(a2a.utils.errors, error_type_name, None)
57+
if (
58+
exception_cls
59+
and isinstance(exception_cls, type)
60+
and issubclass(exception_cls, a2a.utils.errors.A2AError)
61+
):
62+
raise exception_cls(error_message) from e
63+
raise A2AClientError(f'gRPC Error {e.code().name}: {e.details()}') from e
64+
65+
def _handle_grpc_exception(func: Callable[..., Any]) -> Callable[..., Any]:
66+
@wraps(func)
67+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
68+
try:
69+
return await func(*args, **kwargs)
70+
except grpc.aio.AioRpcError as e:
71+
_map_grpc_error(e)
72+
return wrapper
73+
74+
def _handle_grpc_stream_exception(func: Callable[..., Any]) -> Callable[..., Any]:
75+
@wraps(func)
76+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
77+
try:
78+
async for item in func(*args, **kwargs):
79+
yield item
80+
except grpc.aio.AioRpcError as e:
81+
_map_grpc_error(e)
82+
return wrapper
83+
84+
4685
@trace_class(kind=SpanKind.CLIENT)
4786
class GrpcTransport(ClientTransport):
4887
"""A gRPC transport for the A2A client."""
@@ -87,6 +126,7 @@ def create(
87126
raise ValueError('grpc_channel_factory is required when using gRPC')
88127
return cls(config.grpc_channel_factory(url), card, config.extensions)
89128

129+
@_handle_grpc_exception
90130
async def send_message(
91131
self,
92132
request: SendMessageRequest,
@@ -100,6 +140,7 @@ async def send_message(
100140
metadata=self._get_grpc_metadata(extensions),
101141
)
102142

143+
@_handle_grpc_stream_exception
103144
async def send_message_streaming(
104145
self,
105146
request: SendMessageRequest,
@@ -118,6 +159,7 @@ async def send_message_streaming(
118159
break
119160
yield response
120161

162+
@_handle_grpc_stream_exception
121163
async def subscribe(
122164
self,
123165
request: SubscribeToTaskRequest,
@@ -136,6 +178,7 @@ async def subscribe(
136178
break
137179
yield response
138180

181+
@_handle_grpc_exception
139182
async def get_task(
140183
self,
141184
request: GetTaskRequest,
@@ -149,6 +192,7 @@ async def get_task(
149192
metadata=self._get_grpc_metadata(extensions),
150193
)
151194

195+
@_handle_grpc_exception
152196
async def list_tasks(
153197
self,
154198
request: ListTasksRequest,
@@ -162,6 +206,7 @@ async def list_tasks(
162206
metadata=self._get_grpc_metadata(extensions),
163207
)
164208

209+
@_handle_grpc_exception
165210
async def cancel_task(
166211
self,
167212
request: CancelTaskRequest,
@@ -175,6 +220,7 @@ async def cancel_task(
175220
metadata=self._get_grpc_metadata(extensions),
176221
)
177222

223+
@_handle_grpc_exception
178224
async def create_task_push_notification_config(
179225
self,
180226
request: CreateTaskPushNotificationConfigRequest,
@@ -188,6 +234,7 @@ async def create_task_push_notification_config(
188234
metadata=self._get_grpc_metadata(extensions),
189235
)
190236

237+
@_handle_grpc_exception
191238
async def get_task_push_notification_config(
192239
self,
193240
request: GetTaskPushNotificationConfigRequest,
@@ -201,6 +248,7 @@ async def get_task_push_notification_config(
201248
metadata=self._get_grpc_metadata(extensions),
202249
)
203250

251+
@_handle_grpc_exception
204252
async def list_task_push_notification_configs(
205253
self,
206254
request: ListTaskPushNotificationConfigsRequest,
@@ -214,6 +262,7 @@ async def list_task_push_notification_configs(
214262
metadata=self._get_grpc_metadata(extensions),
215263
)
216264

265+
@_handle_grpc_exception
217266
async def delete_task_push_notification_config(
218267
self,
219268
request: DeleteTaskPushNotificationConfigRequest,
@@ -227,6 +276,7 @@ async def delete_task_push_notification_config(
227276
metadata=self._get_grpc_metadata(extensions),
228277
)
229278

279+
@_handle_grpc_exception
230280
async def get_extended_agent_card(
231281
self,
232282
*,

0 commit comments

Comments
 (0)