Skip to content

Commit b4c7f8c

Browse files
committed
fix: allign error code with the latest spec
Source: https://a2a-protocol.org/latest/specification/#54-error-code-mappings. Added roundtrip tests to `test_client_server_integration.py`.
1 parent 1f51bdf commit b4c7f8c

8 files changed

Lines changed: 80 additions & 24 deletions

File tree

src/a2a/server/request_handlers/grpc_handler.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,14 @@ def build(self, context: grpc.aio.ServicerContext) -> ServerCallContext:
9090
types.InvalidParamsError: grpc.StatusCode.INVALID_ARGUMENT,
9191
types.InternalError: grpc.StatusCode.INTERNAL,
9292
types.TaskNotFoundError: grpc.StatusCode.NOT_FOUND,
93-
types.TaskNotCancelableError: grpc.StatusCode.UNIMPLEMENTED,
93+
types.TaskNotCancelableError: grpc.StatusCode.FAILED_PRECONDITION,
9494
types.PushNotificationNotSupportedError: grpc.StatusCode.UNIMPLEMENTED,
9595
types.UnsupportedOperationError: grpc.StatusCode.UNIMPLEMENTED,
96-
types.ContentTypeNotSupportedError: grpc.StatusCode.UNIMPLEMENTED,
96+
types.ContentTypeNotSupportedError: grpc.StatusCode.INVALID_ARGUMENT,
9797
types.InvalidAgentResponseError: grpc.StatusCode.INTERNAL,
98+
types.AuthenticatedExtendedCardNotConfiguredError: grpc.StatusCode.FAILED_PRECONDITION,
99+
types.ExtensionSupportRequiredError: grpc.StatusCode.FAILED_PRECONDITION,
100+
types.VersionNotSupportedError: grpc.StatusCode.UNIMPLEMENTED,
98101
}
99102

100103

src/a2a/server/request_handlers/jsonrpc_handler.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
A2AError,
3838
AuthenticatedExtendedCardNotConfiguredError,
3939
ContentTypeNotSupportedError,
40+
ExtensionSupportRequiredError,
4041
InternalError,
4142
InvalidAgentResponseError,
4243
InvalidParamsError,
@@ -46,6 +47,7 @@
4647
TaskNotCancelableError,
4748
TaskNotFoundError,
4849
UnsupportedOperationError,
50+
VersionNotSupportedError,
4951
)
5052
from a2a.utils.helpers import maybe_await, validate
5153
from a2a.utils.telemetry import SpanKind, trace_class
@@ -66,6 +68,8 @@
6668
InvalidParamsError: JSONRPCError,
6769
InvalidRequestError: JSONRPCError,
6870
MethodNotFoundError: JSONRPCError,
71+
ExtensionSupportRequiredError: JSONRPCError,
72+
VersionNotSupportedError: JSONRPCError,
6973
}
7074

7175

src/a2a/server/request_handlers/response_helpers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
A2AError,
3232
AuthenticatedExtendedCardNotConfiguredError,
3333
ContentTypeNotSupportedError,
34+
ExtensionSupportRequiredError,
3435
InternalError,
3536
InvalidAgentResponseError,
3637
InvalidParamsError,
@@ -40,6 +41,7 @@
4041
TaskNotCancelableError,
4142
TaskNotFoundError,
4243
UnsupportedOperationError,
44+
VersionNotSupportedError,
4345
)
4446

4547

@@ -55,6 +57,8 @@
5557
InvalidRequestError: JSONRPCError,
5658
MethodNotFoundError: JSONRPCError,
5759
InternalError: JSONRPCInternalError,
60+
ExtensionSupportRequiredError: JSONRPCError,
61+
VersionNotSupportedError: JSONRPCError,
5862
}
5963

6064

src/a2a/types/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from a2a.utils.errors import (
5555
AuthenticatedExtendedCardNotConfiguredError,
5656
ContentTypeNotSupportedError,
57+
ExtensionSupportRequiredError,
5758
InternalError,
5859
InvalidAgentResponseError,
5960
InvalidParamsError,
@@ -63,6 +64,7 @@
6364
TaskNotCancelableError,
6465
TaskNotFoundError,
6566
UnsupportedOperationError,
67+
VersionNotSupportedError,
6668
)
6769

6870

@@ -99,6 +101,7 @@
99101
'ContentTypeNotSupportedError',
100102
'DeleteTaskPushNotificationConfigRequest',
101103
'DeviceCodeOAuthFlow',
104+
'ExtensionSupportRequiredError',
102105
'GetExtendedAgentCardRequest',
103106
'GetTaskPushNotificationConfigRequest',
104107
'GetTaskRequest',
@@ -139,4 +142,5 @@
139142
'TaskStatus',
140143
'TaskStatusUpdateEvent',
141144
'UnsupportedOperationError',
145+
'VersionNotSupportedError',
142146
]

src/a2a/utils/error_handlers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
A2AError,
2929
AuthenticatedExtendedCardNotConfiguredError,
3030
ContentTypeNotSupportedError,
31+
ExtensionSupportRequiredError,
3132
InternalError,
3233
InvalidAgentResponseError,
3334
InvalidParamsError,
@@ -37,6 +38,7 @@
3738
TaskNotCancelableError,
3839
TaskNotFoundError,
3940
UnsupportedOperationError,
41+
VersionNotSupportedError,
4042
)
4143

4244

@@ -74,6 +76,8 @@
7476
ContentTypeNotSupportedError: 415,
7577
InvalidAgentResponseError: 502,
7678
AuthenticatedExtendedCardNotConfiguredError: 404,
79+
ExtensionSupportRequiredError: 400,
80+
VersionNotSupportedError: 400,
7781
}
7882

7983

src/a2a/utils/errors.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ class VersionNotSupportedError(A2AError):
123123
ContentTypeNotSupportedError: -32005,
124124
InvalidAgentResponseError: -32006,
125125
AuthenticatedExtendedCardNotConfiguredError: -32007,
126+
ExtensionSupportRequiredError: -32008,
127+
VersionNotSupportedError: -32009,
126128
InvalidParamsError: -32602,
127129
InvalidRequestError: -32600,
128130
MethodNotFoundError: -32601,

tests/integration/test_client_server_integration.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,24 @@
5050
TaskStatus,
5151
TaskStatusUpdateEvent,
5252
)
53-
from a2a.utils.constants import TransportProtocol
53+
from a2a.utils.constants import (
54+
TransportProtocol,
55+
)
56+
from a2a.utils.errors import (
57+
AuthenticatedExtendedCardNotConfiguredError,
58+
ContentTypeNotSupportedError,
59+
ExtensionSupportRequiredError,
60+
InternalError,
61+
InvalidAgentResponseError,
62+
InvalidParamsError,
63+
InvalidRequestError,
64+
MethodNotFoundError,
65+
PushNotificationNotSupportedError,
66+
TaskNotCancelableError,
67+
TaskNotFoundError,
68+
UnsupportedOperationError,
69+
VersionNotSupportedError,
70+
)
5471
from a2a.utils.signing import (
5572
create_agent_card_signer,
5673
create_signature_verifier,
@@ -788,6 +805,43 @@ async def test_client_get_signed_base_and_extended_cards(
788805
await client.close()
789806

790807

808+
@pytest.mark.asyncio
809+
@pytest.mark.parametrize(
810+
'error_cls',
811+
[
812+
TaskNotFoundError,
813+
TaskNotCancelableError,
814+
PushNotificationNotSupportedError,
815+
UnsupportedOperationError,
816+
ContentTypeNotSupportedError,
817+
InvalidAgentResponseError,
818+
AuthenticatedExtendedCardNotConfiguredError,
819+
ExtensionSupportRequiredError,
820+
VersionNotSupportedError,
821+
],
822+
)
823+
async def test_client_handles_a2a_errors(transport_setups, error_cls) -> None:
824+
"""Integration test to verify error propagation from handler to client."""
825+
client = transport_setups.client
826+
handler = transport_setups.handler
827+
828+
# Mock the handler to raise the error
829+
handler.on_get_task.side_effect = error_cls('Test error message')
830+
831+
params = GetTaskRequest(id='some-id')
832+
833+
# We expect the client to raise the same error_cls.
834+
with pytest.raises(error_cls) as exc_info:
835+
await client.get_task(request=params)
836+
837+
assert 'Test error message' in str(exc_info.value)
838+
839+
# Reset side_effect for other tests
840+
handler.on_get_task.side_effect = None
841+
842+
await client.close()
843+
844+
791845
@pytest.mark.asyncio
792846
@pytest.mark.parametrize(
793847
'request_kwargs, expected_error_code',

tests/server/request_handlers/test_grpc_handler.py

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -143,25 +143,6 @@ async def test_get_task_not_found(
143143
)
144144

145145

146-
@pytest.mark.asyncio
147-
async def test_cancel_task_server_error(
148-
grpc_handler: GrpcHandler,
149-
mock_request_handler: AsyncMock,
150-
mock_grpc_context: AsyncMock,
151-
) -> None:
152-
"""Test CancelTask call when handler raises A2AError."""
153-
request_proto = a2a_pb2.CancelTaskRequest(id='task-1')
154-
error = types.TaskNotCancelableError()
155-
mock_request_handler.on_cancel_task.side_effect = error
156-
157-
await grpc_handler.CancelTask(request_proto, mock_grpc_context)
158-
159-
mock_grpc_context.abort.assert_awaited_once_with(
160-
grpc.StatusCode.UNIMPLEMENTED,
161-
'Task cannot be canceled',
162-
)
163-
164-
165146
@pytest.mark.asyncio
166147
async def test_send_streaming_message(
167148
grpc_handler: GrpcHandler,
@@ -340,7 +321,7 @@ async def test_list_tasks_success(
340321
),
341322
(
342323
types.TaskNotCancelableError(),
343-
grpc.StatusCode.UNIMPLEMENTED,
324+
grpc.StatusCode.FAILED_PRECONDITION,
344325
'TaskNotCancelableError',
345326
),
346327
(
@@ -355,7 +336,7 @@ async def test_list_tasks_success(
355336
),
356337
(
357338
types.ContentTypeNotSupportedError(),
358-
grpc.StatusCode.UNIMPLEMENTED,
339+
grpc.StatusCode.INVALID_ARGUMENT,
359340
'ContentTypeNotSupportedError',
360341
),
361342
(

0 commit comments

Comments
 (0)