Skip to content

Commit d90c5f5

Browse files
committed
Refactor REST error handling based on PR comments
- Refactor mapping to use a namedtuple for RestErrorMap - Extract _build_error_payload helper function for error responses - Fix truthiness check for error data in logs
1 parent 5889fed commit d90c5f5

2 files changed

Lines changed: 89 additions & 59 deletions

File tree

src/a2a/utils/error_handlers.py

Lines changed: 52 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,38 @@
2121
A2A_REST_ERROR_MAPPING,
2222
A2AError,
2323
InternalError,
24+
RestErrorMap,
2425
)
2526

2627

2728
logger = logging.getLogger(__name__)
2829

2930

31+
def _build_error_payload(
32+
code: int,
33+
status: str,
34+
message: str,
35+
reason: str | None = None,
36+
metadata: dict[str, Any] | None = None,
37+
) -> dict[str, Any]:
38+
"""Helper function to build the JSON error payload."""
39+
payload: dict[str, Any] = {
40+
'code': code,
41+
'status': status,
42+
'message': message,
43+
}
44+
if reason:
45+
payload['details'] = [
46+
{
47+
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
48+
'reason': reason,
49+
'domain': 'a2a-protocol.org',
50+
'metadata': metadata if metadata is not None else {},
51+
}
52+
]
53+
return {'error': payload}
54+
55+
3056
def rest_error_handler(
3157
func: Callable[..., Awaitable[Response]],
3258
) -> Callable[..., Awaitable[Response]]:
@@ -37,9 +63,12 @@ async def wrapper(*args: Any, **kwargs: Any) -> Response:
3763
try:
3864
return await func(*args, **kwargs)
3965
except A2AError as error:
40-
http_code, grpc_status, reason = A2A_REST_ERROR_MAPPING.get(
41-
type(error), (500, 'INTERNAL', 'INTERNAL_ERROR')
66+
mapping = A2A_REST_ERROR_MAPPING.get(
67+
type(error), RestErrorMap(500, 'INTERNAL', 'INTERNAL_ERROR')
4268
)
69+
http_code = mapping.http_code
70+
grpc_status = mapping.grpc_status
71+
reason = mapping.reason
4372

4473
log_level = (
4574
logging.ERROR
@@ -51,62 +80,44 @@ async def wrapper(*args: Any, **kwargs: Any) -> Response:
5180
"Request error: Code=%s, Message='%s'%s",
5281
getattr(error, 'code', 'N/A'),
5382
getattr(error, 'message', str(error)),
54-
f', Data={error.data}' if hasattr(error, 'data') else '',
83+
f', Data={error.data}' if error.data else '',
5584
)
5685

5786
# SECURITY WARNING: Data attached to A2AError.data is serialized unaltered and exposed publicly to the client in the REST API response.
5887
metadata = getattr(error, 'data', None) or {}
5988

6089
return JSONResponse(
61-
content={
62-
'error': {
63-
'code': http_code,
64-
'status': grpc_status,
65-
'message': getattr(error, 'message', str(error)),
66-
'details': [
67-
{
68-
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
69-
'reason': reason,
70-
'domain': 'a2a-protocol.org',
71-
'metadata': metadata,
72-
}
73-
],
74-
}
75-
},
90+
content=_build_error_payload(
91+
code=http_code,
92+
status=grpc_status,
93+
message=getattr(error, 'message', str(error)),
94+
reason=reason,
95+
metadata=metadata,
96+
),
7697
status_code=http_code,
7798
media_type='application/json',
7899
)
79100
except ParseError as error:
80101
logger.warning('Parse error: %s', str(error))
81102
return JSONResponse(
82-
content={
83-
'error': {
84-
'code': 400,
85-
'status': 'INVALID_ARGUMENT',
86-
'message': str(error),
87-
'details': [
88-
{
89-
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
90-
'reason': 'INVALID_REQUEST',
91-
'domain': 'a2a-protocol.org',
92-
'metadata': {},
93-
}
94-
],
95-
}
96-
},
103+
content=_build_error_payload(
104+
code=400,
105+
status='INVALID_ARGUMENT',
106+
message=str(error),
107+
reason='INVALID_REQUEST',
108+
metadata={},
109+
),
97110
status_code=400,
98111
media_type='application/json',
99112
)
100113
except Exception:
101114
logger.exception('Unknown error occurred')
102115
return JSONResponse(
103-
content={
104-
'error': {
105-
'code': 500,
106-
'status': 'INTERNAL',
107-
'message': 'unknown exception',
108-
}
109-
},
116+
content=_build_error_payload(
117+
code=500,
118+
status='INTERNAL',
119+
message='unknown exception',
120+
),
110121
status_code=500,
111122
media_type='application/json',
112123
)
@@ -134,9 +145,7 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any:
134145
"Request error: Code=%s, Message='%s'%s",
135146
getattr(error, 'code', 'N/A'),
136147
getattr(error, 'message', str(error)),
137-
', Data=' + str(getattr(error, 'data', ''))
138-
if getattr(error, 'data', None)
139-
else '',
148+
f', Data={error.data}' if error.data else '',
140149
)
141150
# Since the stream has started, we can't return a JSONResponse.
142151
# Instead, we run the error handling logic (provides logging)

src/a2a/utils/errors.py

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
as well as server exception classes.
55
"""
66

7+
from typing import NamedTuple
8+
9+
10+
class RestErrorMap(NamedTuple):
11+
"""Named tuple mapping HTTP status, gRPC status, and reason strings."""
12+
13+
http_code: int
14+
grpc_status: str
15+
reason: str
16+
717

818
class A2AError(Exception):
919
"""Base exception for A2A errors."""
@@ -110,6 +120,7 @@ class VersionNotSupportedError(A2AError):
110120
'InvalidRequestError',
111121
'MethodNotFoundError',
112122
'PushNotificationNotSupportedError',
123+
'RestErrorMap',
113124
'TaskNotCancelableError',
114125
'TaskNotFoundError',
115126
'UnsupportedOperationError',
@@ -134,43 +145,53 @@ class VersionNotSupportedError(A2AError):
134145
}
135146

136147

137-
A2A_REST_ERROR_MAPPING: dict[type[A2AError], tuple[int, str, str]] = {
138-
TaskNotFoundError: (404, 'NOT_FOUND', 'TASK_NOT_FOUND'),
139-
TaskNotCancelableError: (409, 'FAILED_PRECONDITION', 'TASK_NOT_CANCELABLE'),
140-
PushNotificationNotSupportedError: (
148+
A2A_REST_ERROR_MAPPING: dict[type[A2AError], RestErrorMap] = {
149+
TaskNotFoundError: RestErrorMap(404, 'NOT_FOUND', 'TASK_NOT_FOUND'),
150+
TaskNotCancelableError: RestErrorMap(
151+
409, 'FAILED_PRECONDITION', 'TASK_NOT_CANCELABLE'
152+
),
153+
PushNotificationNotSupportedError: RestErrorMap(
141154
400,
142155
'UNIMPLEMENTED',
143156
'PUSH_NOTIFICATION_NOT_SUPPORTED',
144157
),
145-
UnsupportedOperationError: (400, 'UNIMPLEMENTED', 'UNSUPPORTED_OPERATION'),
146-
ContentTypeNotSupportedError: (
158+
UnsupportedOperationError: RestErrorMap(
159+
400, 'UNIMPLEMENTED', 'UNSUPPORTED_OPERATION'
160+
),
161+
ContentTypeNotSupportedError: RestErrorMap(
147162
415,
148163
'INVALID_ARGUMENT',
149164
'CONTENT_TYPE_NOT_SUPPORTED',
150165
),
151-
InvalidAgentResponseError: (502, 'INTERNAL', 'INVALID_AGENT_RESPONSE'),
152-
ExtendedAgentCardNotConfiguredError: (
166+
InvalidAgentResponseError: RestErrorMap(
167+
502, 'INTERNAL', 'INVALID_AGENT_RESPONSE'
168+
),
169+
ExtendedAgentCardNotConfiguredError: RestErrorMap(
153170
400,
154171
'FAILED_PRECONDITION',
155172
'EXTENDED_AGENT_CARD_NOT_CONFIGURED',
156173
),
157-
ExtensionSupportRequiredError: (
174+
ExtensionSupportRequiredError: RestErrorMap(
158175
400,
159176
'FAILED_PRECONDITION',
160177
'EXTENSION_SUPPORT_REQUIRED',
161178
),
162-
VersionNotSupportedError: (400, 'UNIMPLEMENTED', 'VERSION_NOT_SUPPORTED'),
163-
InvalidParamsError: (400, 'INVALID_ARGUMENT', 'INVALID_PARAMS'),
164-
InvalidRequestError: (400, 'INVALID_ARGUMENT', 'INVALID_REQUEST'),
165-
MethodNotFoundError: (404, 'NOT_FOUND', 'METHOD_NOT_FOUND'),
166-
InternalError: (500, 'INTERNAL', 'INTERNAL_ERROR'),
179+
VersionNotSupportedError: RestErrorMap(
180+
400, 'UNIMPLEMENTED', 'VERSION_NOT_SUPPORTED'
181+
),
182+
InvalidParamsError: RestErrorMap(400, 'INVALID_ARGUMENT', 'INVALID_PARAMS'),
183+
InvalidRequestError: RestErrorMap(
184+
400, 'INVALID_ARGUMENT', 'INVALID_REQUEST'
185+
),
186+
MethodNotFoundError: RestErrorMap(404, 'NOT_FOUND', 'METHOD_NOT_FOUND'),
187+
InternalError: RestErrorMap(500, 'INTERNAL', 'INTERNAL_ERROR'),
167188
}
168189

169190

170191
A2A_ERROR_REASONS = {
171-
cls: reason for cls, (_, _, reason) in A2A_REST_ERROR_MAPPING.items()
192+
cls: mapping.reason for cls, mapping in A2A_REST_ERROR_MAPPING.items()
172193
}
173194

174195
A2A_REASON_TO_ERROR = {
175-
reason: cls for cls, (_, _, reason) in A2A_REST_ERROR_MAPPING.items()
196+
mapping.reason: cls for cls, mapping in A2A_REST_ERROR_MAPPING.items()
176197
}

0 commit comments

Comments
 (0)