Skip to content

Commit 796d86b

Browse files
committed
refactor: decouple JSON-RPC errors and fix circular imports
Signed-off-by: Luca Muscariello <muscariello@ieee.org>
1 parent 3eeea28 commit 796d86b

21 files changed

Lines changed: 290 additions & 327 deletions

src/a2a/client/errors.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
from typing import Any
44

5-
from a2a.utils.errors import A2AError
6-
75

86
class A2AClientError(Exception):
97
"""Base exception for A2A Client errors."""
@@ -79,13 +77,13 @@ def __init__(self, message: str):
7977
class A2AClientJSONRPCError(A2AClientError):
8078
"""Client exception for JSON-RPC errors returned by the server."""
8179

82-
error: dict[str, Any] | A2AError
80+
error: dict[str, Any]
8381

84-
def __init__(self, error: dict[str, Any] | A2AError):
82+
def __init__(self, error: dict[str, Any]):
8583
"""Initializes the A2AClientJsonRPCError.
8684
8785
Args:
88-
error: The JSON-RPC error dict from the jsonrpc library, or A2AError object.
86+
error: The JSON-RPC error dict from the jsonrpc library.
8987
"""
9088
self.error = error
9189
super().__init__(f'JSON-RPC Error {self.error}')
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from typing import Any, Literal
2+
3+
from pydantic import BaseModel
4+
5+
6+
class JSONRPCBaseModel(BaseModel):
7+
model_config = {
8+
'extra': 'allow',
9+
'populate_by_name': True,
10+
'arbitrary_types_allowed': True,
11+
}
12+
13+
14+
class JSONRPCError(JSONRPCBaseModel):
15+
code: int
16+
message: str
17+
data: Any | None = None
18+
19+
20+
class JSONParseError(JSONRPCError):
21+
code: Literal[-32700] = -32700
22+
message: str = 'Parse error'
23+
24+
25+
class InvalidRequestError(JSONRPCError):
26+
code: Literal[-32600] = -32600
27+
message: str = 'Invalid Request'
28+
29+
30+
class MethodNotFoundError(JSONRPCError):
31+
code: Literal[-32601] = -32601
32+
message: str = 'Method not found'
33+
34+
35+
class InvalidParamsError(JSONRPCError):
36+
code: Literal[-32602] = -32602
37+
message: str = 'Invalid params'
38+
39+
40+
class InternalError(JSONRPCError):
41+
code: Literal[-32603] = -32603
42+
message: str = 'Internal error'

src/a2a/server/apps/jsonrpc/fastapi_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
JSONRPCApplication,
2424
)
2525
from a2a.server.context import ServerCallContext
26-
from a2a.server.request_handlers.jsonrpc_handler import RequestHandler
26+
from a2a.server.request_handlers.request_handler import RequestHandler
2727
from a2a.types.a2a_pb2 import AgentCard
2828
from a2a.utils.constants import (
2929
AGENT_CARD_WELL_KNOWN_PATH,

src/a2a/server/apps/jsonrpc/jsonrpc_app.py

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,25 @@
1010
from typing import TYPE_CHECKING, Any
1111

1212
from google.protobuf.json_format import MessageToDict, ParseDict
13-
from jsonrpc.jsonrpc2 import JSONRPC20Request, JSONRPC20Response
13+
from jsonrpc.jsonrpc2 import JSONRPC20Request
1414

1515
from a2a.auth.user import UnauthenticatedUser
1616
from a2a.auth.user import User as A2AUser
1717
from a2a.extensions.common import (
1818
HTTP_EXTENSION_HEADER,
1919
get_requested_extensions,
2020
)
21+
from a2a.server.apps.jsonrpc.errors import (
22+
InternalError,
23+
InvalidParamsError,
24+
InvalidRequestError,
25+
JSONParseError,
26+
MethodNotFoundError,
27+
)
2128
from a2a.server.context import ServerCallContext
2229
from a2a.server.request_handlers.jsonrpc_handler import JSONRPCHandler
2330
from a2a.server.request_handlers.request_handler import RequestHandler
31+
from a2a.server.request_handlers.response_helpers import build_error_response
2432
from a2a.types import A2ARequest
2533
from a2a.types.a2a_pb2 import (
2634
AgentCard,
@@ -41,17 +49,13 @@
4149
PREV_AGENT_CARD_WELL_KNOWN_PATH,
4250
)
4351
from a2a.utils.errors import (
44-
A2AError,
45-
InternalError,
46-
InvalidParamsError,
47-
InvalidRequestError,
48-
JSONParseError,
49-
MethodNotFoundError,
5052
MethodNotImplementedError,
5153
UnsupportedOperationError,
5254
)
5355

5456

57+
INTERNAL_ERROR_CODE = -32603
58+
5559
logger = logging.getLogger(__name__)
5660

5761
if TYPE_CHECKING:
@@ -206,6 +210,7 @@ def __init__( # noqa: PLR0913
206210
' `JSONRPCApplication`. They can be added as a part of `a2a-sdk`'
207211
' optional dependencies, `a2a-sdk[http-server]`.'
208212
)
213+
209214
self.agent_card = agent_card
210215
self.extended_agent_card = extended_agent_card
211216
self.card_modifier = card_modifier
@@ -220,39 +225,39 @@ def __init__( # noqa: PLR0913
220225
self._max_content_length = max_content_length
221226

222227
def _generate_error_response(
223-
self, request_id: str | int | None, error: A2AError
228+
self, request_id: str | int | None, error: Exception
224229
) -> JSONResponse:
225230
"""Creates a Starlette JSONResponse for a JSON-RPC error.
226231
227232
Logs the error based on its type.
228233
229234
Args:
230235
request_id: The ID of the request that caused the error.
231-
error: The error object (one of the A2AError union types).
236+
error: The error object (one of the JSONRPCError types).
232237
233238
Returns:
234239
A `JSONResponse` object formatted as a JSON-RPC error response.
235240
"""
236-
error_dict = error.model_dump(exclude_none=True)
237-
error_resp = JSONRPC20Response(error=error_dict, _id=request_id)
241+
response_data = build_error_response(request_id, error)
242+
error_info = response_data.get('error', {})
243+
code = error_info.get('code')
244+
message = error_info.get('message')
245+
data = error_info.get('data')
246+
247+
log_level = logging.WARNING
248+
if code == INTERNAL_ERROR_CODE:
249+
log_level = logging.ERROR
238250

239-
log_level = (
240-
logging.ERROR
241-
if isinstance(error, InternalError)
242-
else logging.WARNING
243-
)
244251
logger.log(
245252
log_level,
246253
"Request Error (ID: %s): Code=%s, Message='%s'%s",
247254
request_id,
248-
error_dict.get('code'),
249-
error_dict.get('message'),
250-
', Data=' + str(error_dict.get('data'))
251-
if error_dict.get('data')
252-
else '',
255+
code,
256+
message,
257+
f', Data={data}' if data else '',
253258
)
254259
return JSONResponse(
255-
error_resp.data,
260+
response_data,
256261
status_code=200,
257262
)
258263

src/a2a/server/apps/jsonrpc/starlette_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
JSONRPCApplication,
2828
)
2929
from a2a.server.context import ServerCallContext
30-
from a2a.server.request_handlers.jsonrpc_handler import RequestHandler
30+
from a2a.server.request_handlers.request_handler import RequestHandler
3131
from a2a.types.a2a_pb2 import AgentCard
3232
from a2a.utils.constants import (
3333
AGENT_CARD_WELL_KNOWN_PATH,

src/a2a/server/request_handlers/grpc_handler.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
HTTP_EXTENSION_HEADER,
2929
get_requested_extensions,
3030
)
31+
from a2a.server.apps.jsonrpc.errors import JSONParseError
3132
from a2a.server.context import ServerCallContext
3233
from a2a.server.request_handlers.request_handler import RequestHandler
3334
from a2a.types import a2a_pb2
@@ -337,7 +338,7 @@ async def abort_context(
337338
) -> None:
338339
"""Sets the grpc errors appropriately in the context."""
339340
match error.error:
340-
case types.JSONParseError():
341+
case JSONParseError():
341342
await context.abort(
342343
grpc.StatusCode.INTERNAL,
343344
f'JSONParseError: {error.error.message}',

src/a2a/server/request_handlers/jsonrpc_handler.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
from google.protobuf.json_format import MessageToDict
99
from jsonrpc.jsonrpc2 import JSONRPC20Response
1010

11+
from a2a.server.apps.jsonrpc.errors import (
12+
InternalError as JSONRPCInternalError,
13+
)
14+
from a2a.server.apps.jsonrpc.errors import (
15+
JSONRPCError,
16+
)
1117
from a2a.server.context import ServerCallContext
1218
from a2a.server.request_handlers.request_handler import RequestHandler
1319
from a2a.types.a2a_pb2 import (
@@ -27,10 +33,19 @@
2733
)
2834
from a2a.utils import proto_utils
2935
from a2a.utils.errors import (
36+
A2AException,
3037
AuthenticatedExtendedCardNotConfiguredError,
38+
ContentTypeNotSupportedError,
3139
InternalError,
40+
InvalidAgentResponseError,
41+
InvalidParamsError,
42+
InvalidRequestError,
43+
MethodNotFoundError,
44+
PushNotificationNotSupportedError,
3245
ServerError,
46+
TaskNotCancelableError,
3347
TaskNotFoundError,
48+
UnsupportedOperationError,
3449
)
3550
from a2a.utils.helpers import validate
3651
from a2a.utils.telemetry import SpanKind, trace_class
@@ -39,6 +54,34 @@
3954
logger = logging.getLogger(__name__)
4055

4156

57+
EXCEPTION_MAP: dict[type[A2AException], type[JSONRPCError]] = {
58+
TaskNotFoundError: JSONRPCError,
59+
TaskNotCancelableError: JSONRPCError,
60+
PushNotificationNotSupportedError: JSONRPCError,
61+
UnsupportedOperationError: JSONRPCError,
62+
ContentTypeNotSupportedError: JSONRPCError,
63+
InvalidAgentResponseError: JSONRPCError,
64+
AuthenticatedExtendedCardNotConfiguredError: JSONRPCError,
65+
InternalError: JSONRPCInternalError,
66+
InvalidParamsError: JSONRPCError,
67+
InvalidRequestError: JSONRPCError,
68+
MethodNotFoundError: JSONRPCError,
69+
}
70+
71+
ERROR_CODE_MAP: dict[type[A2AException], int] = {
72+
TaskNotFoundError: -32001,
73+
TaskNotCancelableError: -32002,
74+
PushNotificationNotSupportedError: -32003,
75+
UnsupportedOperationError: -32004,
76+
ContentTypeNotSupportedError: -32005,
77+
InvalidAgentResponseError: -32006,
78+
AuthenticatedExtendedCardNotConfiguredError: -32007,
79+
InvalidParamsError: -32602,
80+
InvalidRequestError: -32600,
81+
MethodNotFoundError: -32601,
82+
}
83+
84+
4285
def _build_success_response(
4386
request_id: str | int | None, result: Any
4487
) -> dict[str, Any]:
@@ -47,10 +90,22 @@ def _build_success_response(
4790

4891

4992
def _build_error_response(
50-
request_id: str | int | None, error: Any
93+
request_id: str | int | None, error: Exception
5194
) -> dict[str, Any]:
5295
"""Build a JSON-RPC error response dict."""
53-
error_dict = error.model_dump(exclude_none=True)
96+
jsonrpc_error: JSONRPCError
97+
if isinstance(error, A2AException):
98+
error_type = type(error)
99+
model_class = EXCEPTION_MAP.get(error_type, JSONRPCInternalError)
100+
code = ERROR_CODE_MAP.get(error_type, -32603)
101+
jsonrpc_error = model_class(
102+
code=code,
103+
message=str(error),
104+
)
105+
else:
106+
jsonrpc_error = JSONRPCInternalError(message=str(error))
107+
108+
error_dict = jsonrpc_error.model_dump(exclude_none=True)
54109
return JSONRPC20Response(error=error_dict, _id=request_id).data
55110

56111

0 commit comments

Comments
 (0)