From b4c7f8c732fcfeab80ee7bb68ee0f0e5dbaae43a Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Fri, 13 Mar 2026 10:03:36 +0000 Subject: [PATCH 1/4] 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`. --- .../server/request_handlers/grpc_handler.py | 7 ++- .../request_handlers/jsonrpc_handler.py | 4 ++ .../request_handlers/response_helpers.py | 4 ++ src/a2a/types/__init__.py | 4 ++ src/a2a/utils/error_handlers.py | 4 ++ src/a2a/utils/errors.py | 2 + .../test_client_server_integration.py | 56 ++++++++++++++++++- .../request_handlers/test_grpc_handler.py | 23 +------- 8 files changed, 80 insertions(+), 24 deletions(-) diff --git a/src/a2a/server/request_handlers/grpc_handler.py b/src/a2a/server/request_handlers/grpc_handler.py index 551891ee..8a4eafc3 100644 --- a/src/a2a/server/request_handlers/grpc_handler.py +++ b/src/a2a/server/request_handlers/grpc_handler.py @@ -90,11 +90,14 @@ def build(self, context: grpc.aio.ServicerContext) -> ServerCallContext: types.InvalidParamsError: grpc.StatusCode.INVALID_ARGUMENT, types.InternalError: grpc.StatusCode.INTERNAL, types.TaskNotFoundError: grpc.StatusCode.NOT_FOUND, - types.TaskNotCancelableError: grpc.StatusCode.UNIMPLEMENTED, + types.TaskNotCancelableError: grpc.StatusCode.FAILED_PRECONDITION, types.PushNotificationNotSupportedError: grpc.StatusCode.UNIMPLEMENTED, types.UnsupportedOperationError: grpc.StatusCode.UNIMPLEMENTED, - types.ContentTypeNotSupportedError: grpc.StatusCode.UNIMPLEMENTED, + types.ContentTypeNotSupportedError: grpc.StatusCode.INVALID_ARGUMENT, types.InvalidAgentResponseError: grpc.StatusCode.INTERNAL, + types.AuthenticatedExtendedCardNotConfiguredError: grpc.StatusCode.FAILED_PRECONDITION, + types.ExtensionSupportRequiredError: grpc.StatusCode.FAILED_PRECONDITION, + types.VersionNotSupportedError: grpc.StatusCode.UNIMPLEMENTED, } diff --git a/src/a2a/server/request_handlers/jsonrpc_handler.py b/src/a2a/server/request_handlers/jsonrpc_handler.py index d0330f2c..e28d69fe 100644 --- a/src/a2a/server/request_handlers/jsonrpc_handler.py +++ b/src/a2a/server/request_handlers/jsonrpc_handler.py @@ -37,6 +37,7 @@ A2AError, AuthenticatedExtendedCardNotConfiguredError, ContentTypeNotSupportedError, + ExtensionSupportRequiredError, InternalError, InvalidAgentResponseError, InvalidParamsError, @@ -46,6 +47,7 @@ TaskNotCancelableError, TaskNotFoundError, UnsupportedOperationError, + VersionNotSupportedError, ) from a2a.utils.helpers import maybe_await, validate from a2a.utils.telemetry import SpanKind, trace_class @@ -66,6 +68,8 @@ InvalidParamsError: JSONRPCError, InvalidRequestError: JSONRPCError, MethodNotFoundError: JSONRPCError, + ExtensionSupportRequiredError: JSONRPCError, + VersionNotSupportedError: JSONRPCError, } diff --git a/src/a2a/server/request_handlers/response_helpers.py b/src/a2a/server/request_handlers/response_helpers.py index f7bffd60..a6778e5a 100644 --- a/src/a2a/server/request_handlers/response_helpers.py +++ b/src/a2a/server/request_handlers/response_helpers.py @@ -31,6 +31,7 @@ A2AError, AuthenticatedExtendedCardNotConfiguredError, ContentTypeNotSupportedError, + ExtensionSupportRequiredError, InternalError, InvalidAgentResponseError, InvalidParamsError, @@ -40,6 +41,7 @@ TaskNotCancelableError, TaskNotFoundError, UnsupportedOperationError, + VersionNotSupportedError, ) @@ -55,6 +57,8 @@ InvalidRequestError: JSONRPCError, MethodNotFoundError: JSONRPCError, InternalError: JSONRPCInternalError, + ExtensionSupportRequiredError: JSONRPCError, + VersionNotSupportedError: JSONRPCError, } diff --git a/src/a2a/types/__init__.py b/src/a2a/types/__init__.py index 7344a0ea..2bcc0f5d 100644 --- a/src/a2a/types/__init__.py +++ b/src/a2a/types/__init__.py @@ -54,6 +54,7 @@ from a2a.utils.errors import ( AuthenticatedExtendedCardNotConfiguredError, ContentTypeNotSupportedError, + ExtensionSupportRequiredError, InternalError, InvalidAgentResponseError, InvalidParamsError, @@ -63,6 +64,7 @@ TaskNotCancelableError, TaskNotFoundError, UnsupportedOperationError, + VersionNotSupportedError, ) @@ -99,6 +101,7 @@ 'ContentTypeNotSupportedError', 'DeleteTaskPushNotificationConfigRequest', 'DeviceCodeOAuthFlow', + 'ExtensionSupportRequiredError', 'GetExtendedAgentCardRequest', 'GetTaskPushNotificationConfigRequest', 'GetTaskRequest', @@ -139,4 +142,5 @@ 'TaskStatus', 'TaskStatusUpdateEvent', 'UnsupportedOperationError', + 'VersionNotSupportedError', ] diff --git a/src/a2a/utils/error_handlers.py b/src/a2a/utils/error_handlers.py index 7d73266c..363d70d1 100644 --- a/src/a2a/utils/error_handlers.py +++ b/src/a2a/utils/error_handlers.py @@ -28,6 +28,7 @@ A2AError, AuthenticatedExtendedCardNotConfiguredError, ContentTypeNotSupportedError, + ExtensionSupportRequiredError, InternalError, InvalidAgentResponseError, InvalidParamsError, @@ -37,6 +38,7 @@ TaskNotCancelableError, TaskNotFoundError, UnsupportedOperationError, + VersionNotSupportedError, ) @@ -74,6 +76,8 @@ ContentTypeNotSupportedError: 415, InvalidAgentResponseError: 502, AuthenticatedExtendedCardNotConfiguredError: 404, + ExtensionSupportRequiredError: 400, + VersionNotSupportedError: 400, } diff --git a/src/a2a/utils/errors.py b/src/a2a/utils/errors.py index 9353805e..1c6464ad 100644 --- a/src/a2a/utils/errors.py +++ b/src/a2a/utils/errors.py @@ -123,6 +123,8 @@ class VersionNotSupportedError(A2AError): ContentTypeNotSupportedError: -32005, InvalidAgentResponseError: -32006, AuthenticatedExtendedCardNotConfiguredError: -32007, + ExtensionSupportRequiredError: -32008, + VersionNotSupportedError: -32009, InvalidParamsError: -32602, InvalidRequestError: -32600, MethodNotFoundError: -32601, diff --git a/tests/integration/test_client_server_integration.py b/tests/integration/test_client_server_integration.py index 12b42020..2913ab4c 100644 --- a/tests/integration/test_client_server_integration.py +++ b/tests/integration/test_client_server_integration.py @@ -50,7 +50,24 @@ TaskStatus, TaskStatusUpdateEvent, ) -from a2a.utils.constants import TransportProtocol +from a2a.utils.constants import ( + TransportProtocol, +) +from a2a.utils.errors import ( + AuthenticatedExtendedCardNotConfiguredError, + ContentTypeNotSupportedError, + ExtensionSupportRequiredError, + InternalError, + InvalidAgentResponseError, + InvalidParamsError, + InvalidRequestError, + MethodNotFoundError, + PushNotificationNotSupportedError, + TaskNotCancelableError, + TaskNotFoundError, + UnsupportedOperationError, + VersionNotSupportedError, +) from a2a.utils.signing import ( create_agent_card_signer, create_signature_verifier, @@ -788,6 +805,43 @@ async def test_client_get_signed_base_and_extended_cards( await client.close() +@pytest.mark.asyncio +@pytest.mark.parametrize( + 'error_cls', + [ + TaskNotFoundError, + TaskNotCancelableError, + PushNotificationNotSupportedError, + UnsupportedOperationError, + ContentTypeNotSupportedError, + InvalidAgentResponseError, + AuthenticatedExtendedCardNotConfiguredError, + ExtensionSupportRequiredError, + VersionNotSupportedError, + ], +) +async def test_client_handles_a2a_errors(transport_setups, error_cls) -> None: + """Integration test to verify error propagation from handler to client.""" + client = transport_setups.client + handler = transport_setups.handler + + # Mock the handler to raise the error + handler.on_get_task.side_effect = error_cls('Test error message') + + params = GetTaskRequest(id='some-id') + + # We expect the client to raise the same error_cls. + with pytest.raises(error_cls) as exc_info: + await client.get_task(request=params) + + assert 'Test error message' in str(exc_info.value) + + # Reset side_effect for other tests + handler.on_get_task.side_effect = None + + await client.close() + + @pytest.mark.asyncio @pytest.mark.parametrize( 'request_kwargs, expected_error_code', diff --git a/tests/server/request_handlers/test_grpc_handler.py b/tests/server/request_handlers/test_grpc_handler.py index 4d121ca2..11ceaf7b 100644 --- a/tests/server/request_handlers/test_grpc_handler.py +++ b/tests/server/request_handlers/test_grpc_handler.py @@ -143,25 +143,6 @@ async def test_get_task_not_found( ) -@pytest.mark.asyncio -async def test_cancel_task_server_error( - grpc_handler: GrpcHandler, - mock_request_handler: AsyncMock, - mock_grpc_context: AsyncMock, -) -> None: - """Test CancelTask call when handler raises A2AError.""" - request_proto = a2a_pb2.CancelTaskRequest(id='task-1') - error = types.TaskNotCancelableError() - mock_request_handler.on_cancel_task.side_effect = error - - await grpc_handler.CancelTask(request_proto, mock_grpc_context) - - mock_grpc_context.abort.assert_awaited_once_with( - grpc.StatusCode.UNIMPLEMENTED, - 'Task cannot be canceled', - ) - - @pytest.mark.asyncio async def test_send_streaming_message( grpc_handler: GrpcHandler, @@ -340,7 +321,7 @@ async def test_list_tasks_success( ), ( types.TaskNotCancelableError(), - grpc.StatusCode.UNIMPLEMENTED, + grpc.StatusCode.FAILED_PRECONDITION, 'TaskNotCancelableError', ), ( @@ -355,7 +336,7 @@ async def test_list_tasks_success( ), ( types.ContentTypeNotSupportedError(), - grpc.StatusCode.UNIMPLEMENTED, + grpc.StatusCode.INVALID_ARGUMENT, 'ContentTypeNotSupportedError', ), ( From 86459354fcd100bd00514fb62c549e78dfe8e9bc Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Fri, 13 Mar 2026 10:11:45 +0000 Subject: [PATCH 2/4] Renaming --- src/a2a/compat/v0_3/jsonrpc_adapter.py | 4 ++-- src/a2a/compat/v0_3/rest_adapter.py | 4 ++-- src/a2a/server/apps/rest/rest_adapter.py | 4 ++-- src/a2a/server/request_handlers/grpc_handler.py | 2 +- src/a2a/server/request_handlers/jsonrpc_handler.py | 6 +++--- src/a2a/server/request_handlers/response_helpers.py | 4 ++-- src/a2a/types/__init__.py | 2 +- src/a2a/utils/error_handlers.py | 6 +++--- src/a2a/utils/errors.py | 6 +++--- tests/integration/test_client_server_integration.py | 4 ++-- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/a2a/compat/v0_3/jsonrpc_adapter.py b/src/a2a/compat/v0_3/jsonrpc_adapter.py index 68c0b848..cdb701b5 100644 --- a/src/a2a/compat/v0_3/jsonrpc_adapter.py +++ b/src/a2a/compat/v0_3/jsonrpc_adapter.py @@ -38,7 +38,7 @@ from a2a.server.jsonrpc_models import ( JSONRPCError as CoreJSONRPCError, ) -from a2a.utils.errors import AuthenticatedExtendedCardNotConfiguredError +from a2a.utils.errors import ExtendedAgentCardNotConfiguredError from a2a.utils.helpers import maybe_await @@ -248,7 +248,7 @@ async def get_authenticated_extended_card( ) -> types_v03.AgentCard: """Handles the 'agent/authenticatedExtendedCard' JSON-RPC method.""" if not self.agent_card.capabilities.extended_agent_card: - raise AuthenticatedExtendedCardNotConfiguredError( + raise ExtendedAgentCardNotConfiguredError( message='Authenticated card not supported' ) diff --git a/src/a2a/compat/v0_3/rest_adapter.py b/src/a2a/compat/v0_3/rest_adapter.py index 948d451a..b861ec06 100644 --- a/src/a2a/compat/v0_3/rest_adapter.py +++ b/src/a2a/compat/v0_3/rest_adapter.py @@ -43,7 +43,7 @@ rest_stream_error_handler, ) from a2a.utils.errors import ( - AuthenticatedExtendedCardNotConfiguredError, + ExtendedAgentCardNotConfiguredError, InvalidRequestError, ) from a2a.utils.helpers import maybe_await @@ -126,7 +126,7 @@ async def handle_authenticated_agent_card( ) -> dict[str, Any]: """Hook for per credential agent card response.""" if not self.agent_card.capabilities.extended_agent_card: - raise AuthenticatedExtendedCardNotConfiguredError( + raise ExtendedAgentCardNotConfiguredError( message='Authenticated card not supported' ) card_to_serve = self.extended_agent_card diff --git a/src/a2a/server/apps/rest/rest_adapter.py b/src/a2a/server/apps/rest/rest_adapter.py index f0708765..e5d21042 100644 --- a/src/a2a/server/apps/rest/rest_adapter.py +++ b/src/a2a/server/apps/rest/rest_adapter.py @@ -45,7 +45,7 @@ rest_stream_error_handler, ) from a2a.utils.errors import ( - AuthenticatedExtendedCardNotConfiguredError, + ExtendedAgentCardNotConfiguredError, InvalidRequestError, ) @@ -192,7 +192,7 @@ async def _handle_authenticated_agent_card( A JSONResponse containing the authenticated card. """ if not self.agent_card.capabilities.extended_agent_card: - raise AuthenticatedExtendedCardNotConfiguredError( + raise ExtendedAgentCardNotConfiguredError( message='Authenticated card not supported' ) card_to_serve = self.extended_agent_card diff --git a/src/a2a/server/request_handlers/grpc_handler.py b/src/a2a/server/request_handlers/grpc_handler.py index 8a4eafc3..326dea23 100644 --- a/src/a2a/server/request_handlers/grpc_handler.py +++ b/src/a2a/server/request_handlers/grpc_handler.py @@ -95,7 +95,7 @@ def build(self, context: grpc.aio.ServicerContext) -> ServerCallContext: types.UnsupportedOperationError: grpc.StatusCode.UNIMPLEMENTED, types.ContentTypeNotSupportedError: grpc.StatusCode.INVALID_ARGUMENT, types.InvalidAgentResponseError: grpc.StatusCode.INTERNAL, - types.AuthenticatedExtendedCardNotConfiguredError: grpc.StatusCode.FAILED_PRECONDITION, + types.ExtendedAgentCardNotConfiguredError: grpc.StatusCode.FAILED_PRECONDITION, types.ExtensionSupportRequiredError: grpc.StatusCode.FAILED_PRECONDITION, types.VersionNotSupportedError: grpc.StatusCode.UNIMPLEMENTED, } diff --git a/src/a2a/server/request_handlers/jsonrpc_handler.py b/src/a2a/server/request_handlers/jsonrpc_handler.py index e28d69fe..c4a6a77e 100644 --- a/src/a2a/server/request_handlers/jsonrpc_handler.py +++ b/src/a2a/server/request_handlers/jsonrpc_handler.py @@ -35,8 +35,8 @@ from a2a.utils.errors import ( JSON_RPC_ERROR_CODE_MAP, A2AError, - AuthenticatedExtendedCardNotConfiguredError, ContentTypeNotSupportedError, + ExtendedAgentCardNotConfiguredError, ExtensionSupportRequiredError, InternalError, InvalidAgentResponseError, @@ -63,7 +63,7 @@ UnsupportedOperationError: JSONRPCError, ContentTypeNotSupportedError: JSONRPCError, InvalidAgentResponseError: JSONRPCError, - AuthenticatedExtendedCardNotConfiguredError: JSONRPCError, + ExtendedAgentCardNotConfiguredError: JSONRPCError, InternalError: JSONRPCInternalError, InvalidParamsError: JSONRPCError, InvalidRequestError: JSONRPCError, @@ -450,7 +450,7 @@ async def get_authenticated_extended_card( """ request_id = self._get_request_id(context) if not self.agent_card.capabilities.extended_agent_card: - raise AuthenticatedExtendedCardNotConfiguredError( + raise ExtendedAgentCardNotConfiguredError( message='Authenticated card not supported' ) diff --git a/src/a2a/server/request_handlers/response_helpers.py b/src/a2a/server/request_handlers/response_helpers.py index a6778e5a..1a3ebad1 100644 --- a/src/a2a/server/request_handlers/response_helpers.py +++ b/src/a2a/server/request_handlers/response_helpers.py @@ -29,8 +29,8 @@ from a2a.utils.errors import ( JSON_RPC_ERROR_CODE_MAP, A2AError, - AuthenticatedExtendedCardNotConfiguredError, ContentTypeNotSupportedError, + ExtendedAgentCardNotConfiguredError, ExtensionSupportRequiredError, InternalError, InvalidAgentResponseError, @@ -52,7 +52,7 @@ UnsupportedOperationError: JSONRPCError, ContentTypeNotSupportedError: JSONRPCError, InvalidAgentResponseError: JSONRPCError, - AuthenticatedExtendedCardNotConfiguredError: JSONRPCError, + ExtendedAgentCardNotConfiguredError: JSONRPCError, InvalidParamsError: JSONRPCError, InvalidRequestError: JSONRPCError, MethodNotFoundError: JSONRPCError, diff --git a/src/a2a/types/__init__.py b/src/a2a/types/__init__.py index 2bcc0f5d..2afe9c95 100644 --- a/src/a2a/types/__init__.py +++ b/src/a2a/types/__init__.py @@ -52,8 +52,8 @@ # Import SDK-specific error types from utils.errors from a2a.utils.errors import ( - AuthenticatedExtendedCardNotConfiguredError, ContentTypeNotSupportedError, + ExtendedAgentCardNotConfiguredError, ExtensionSupportRequiredError, InternalError, InvalidAgentResponseError, diff --git a/src/a2a/utils/error_handlers.py b/src/a2a/utils/error_handlers.py index 363d70d1..e8fc7124 100644 --- a/src/a2a/utils/error_handlers.py +++ b/src/a2a/utils/error_handlers.py @@ -26,8 +26,8 @@ ) from a2a.utils.errors import ( A2AError, - AuthenticatedExtendedCardNotConfiguredError, ContentTypeNotSupportedError, + ExtendedAgentCardNotConfiguredError, ExtensionSupportRequiredError, InternalError, InvalidAgentResponseError, @@ -58,7 +58,7 @@ | type[UnsupportedOperationError] | type[ContentTypeNotSupportedError] | type[InvalidAgentResponseError] - | type[AuthenticatedExtendedCardNotConfiguredError] + | type[ExtendedAgentCardNotConfiguredError] ) A2AErrorToHttpStatus: dict[_A2AErrorType, int] = { @@ -75,7 +75,7 @@ UnsupportedOperationError: 501, ContentTypeNotSupportedError: 415, InvalidAgentResponseError: 502, - AuthenticatedExtendedCardNotConfiguredError: 404, + ExtendedAgentCardNotConfiguredError: 400, ExtensionSupportRequiredError: 400, VersionNotSupportedError: 400, } diff --git a/src/a2a/utils/errors.py b/src/a2a/utils/errors.py index 1c6464ad..ac4da027 100644 --- a/src/a2a/utils/errors.py +++ b/src/a2a/utils/errors.py @@ -58,7 +58,7 @@ class InvalidAgentResponseError(A2AError): message = 'Invalid agent response' -class AuthenticatedExtendedCardNotConfiguredError(A2AError): +class ExtendedAgentCardNotConfiguredError(A2AError): """Exception raised when the authenticated extended card is not configured.""" message = 'Authenticated Extended Card is not configured' @@ -122,7 +122,7 @@ class VersionNotSupportedError(A2AError): UnsupportedOperationError: -32004, ContentTypeNotSupportedError: -32005, InvalidAgentResponseError: -32006, - AuthenticatedExtendedCardNotConfiguredError: -32007, + ExtendedAgentCardNotConfiguredError: -32007, ExtensionSupportRequiredError: -32008, VersionNotSupportedError: -32009, InvalidParamsError: -32602, @@ -139,7 +139,7 @@ class VersionNotSupportedError(A2AError): UnsupportedOperationError: 'UNSUPPORTED_OPERATION', ContentTypeNotSupportedError: 'CONTENT_TYPE_NOT_SUPPORTED', InvalidAgentResponseError: 'INVALID_AGENT_RESPONSE', - AuthenticatedExtendedCardNotConfiguredError: 'EXTENDED_AGENT_CARD_NOT_CONFIGURED', + ExtendedAgentCardNotConfiguredError: 'EXTENDED_AGENT_CARD_NOT_CONFIGURED', ExtensionSupportRequiredError: 'EXTENSION_SUPPORT_REQUIRED', VersionNotSupportedError: 'VERSION_NOT_SUPPORTED', } diff --git a/tests/integration/test_client_server_integration.py b/tests/integration/test_client_server_integration.py index 2913ab4c..82c14ce6 100644 --- a/tests/integration/test_client_server_integration.py +++ b/tests/integration/test_client_server_integration.py @@ -54,7 +54,7 @@ TransportProtocol, ) from a2a.utils.errors import ( - AuthenticatedExtendedCardNotConfiguredError, + ExtendedAgentCardNotConfiguredError, ContentTypeNotSupportedError, ExtensionSupportRequiredError, InternalError, @@ -815,7 +815,7 @@ async def test_client_get_signed_base_and_extended_cards( UnsupportedOperationError, ContentTypeNotSupportedError, InvalidAgentResponseError, - AuthenticatedExtendedCardNotConfiguredError, + ExtendedAgentCardNotConfiguredError, ExtensionSupportRequiredError, VersionNotSupportedError, ], From 631d4306aa2a38065f3cb168a16b14e9937e7ee1 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Fri, 13 Mar 2026 10:14:30 +0000 Subject: [PATCH 3/4] Cosmetics --- src/a2a/server/request_handlers/jsonrpc_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/a2a/server/request_handlers/jsonrpc_handler.py b/src/a2a/server/request_handlers/jsonrpc_handler.py index c4a6a77e..ee3b04dc 100644 --- a/src/a2a/server/request_handlers/jsonrpc_handler.py +++ b/src/a2a/server/request_handlers/jsonrpc_handler.py @@ -451,7 +451,7 @@ async def get_authenticated_extended_card( request_id = self._get_request_id(context) if not self.agent_card.capabilities.extended_agent_card: raise ExtendedAgentCardNotConfiguredError( - message='Authenticated card not supported' + message='The agent does not have an extended agent card configured' ) base_card = self.extended_agent_card From e0f72206db685be293819324662ad58118bc22a4 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Fri, 13 Mar 2026 10:19:13 +0000 Subject: [PATCH 4/4] Fix CI --- src/a2a/utils/error_handlers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/a2a/utils/error_handlers.py b/src/a2a/utils/error_handlers.py index e8fc7124..00843fcf 100644 --- a/src/a2a/utils/error_handlers.py +++ b/src/a2a/utils/error_handlers.py @@ -59,6 +59,8 @@ | type[ContentTypeNotSupportedError] | type[InvalidAgentResponseError] | type[ExtendedAgentCardNotConfiguredError] + | type[ExtensionSupportRequiredError] + | type[VersionNotSupportedError] ) A2AErrorToHttpStatus: dict[_A2AErrorType, int] = {