Skip to content

Commit 12b5edf

Browse files
authored
refactor(client): map error responses to domain errors (#761)
Some preparation work for #722 and #723. 1. Unify error handling in the client: map [A2A errors](https://a2a-protocol.org/latest/specification/#54-error-code-mappings) to appropriate transport agnostic errors. 2. Use `A2AClientError` (also derived from `A2AError` like domain ones) for non-A2A errors. Transport specific errors are preserved via `__cause__` (`raise from`). 3. Dedupe HTTP related code for JSON-RPC and REST. **TODO**: #763 - for now all timeout related hacks are removed. Re #737
1 parent 4cf5a15 commit 12b5edf

18 files changed

Lines changed: 601 additions & 788 deletions

src/a2a/client/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@
1313
from a2a.client.client_factory import ClientFactory, minimal_agent_card
1414
from a2a.client.errors import (
1515
A2AClientError,
16-
A2AClientHTTPError,
17-
A2AClientJSONError,
1816
A2AClientTimeoutError,
17+
AgentCardResolutionError,
1918
)
2019
from a2a.client.helpers import create_text_message_object
2120
from a2a.client.middleware import ClientCallContext, ClientCallInterceptor
@@ -27,9 +26,8 @@
2726
__all__ = [
2827
'A2ACardResolver',
2928
'A2AClientError',
30-
'A2AClientHTTPError',
31-
'A2AClientJSONError',
3229
'A2AClientTimeoutError',
30+
'AgentCardResolutionError',
3331
'AuthInterceptor',
3432
'BaseClient',
3533
'Client',

src/a2a/client/card_resolver.py

Lines changed: 10 additions & 14 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+
AgentCardResolutionError: 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,20 @@ 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}',
94+
status_code=e.response.status_code,
9895
) from e
9996
except json.JSONDecodeError as e:
100-
raise A2AClientJSONError(
97+
raise AgentCardResolutionError(
10198
f'Failed to parse JSON for agent card from {target_url}: {e}'
10299
) from e
103100
except httpx.RequestError as e:
104-
raise A2AClientHTTPError(
105-
503,
101+
raise AgentCardResolutionError(
106102
f'Network communication error fetching agent card from {target_url}: {e}',
107103
) from e
108104
except ParseError as e:
109-
raise A2AClientJSONError(
105+
raise AgentCardResolutionError(
110106
f'Failed to validate agent card structure from {target_url}: {e}'
111107
) from e
112108

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: 7 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,19 @@
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."""
10+
class AgentCardResolutionError(A2AClientError):
11+
"""Exception raised when an agent card cannot be resolved."""
1212

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-
"""
13+
def __init__(self, message: str, status_code: int | None = None) -> None:
14+
super().__init__(message)
2015
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})'
4816

4917

5018
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}')
19+
"""Exception for timeout errors during a request."""

0 commit comments

Comments
 (0)