Skip to content

Commit d2e869f

Browse files
maestegemini-code-assist[bot]holtskinner
authored
fix: fixing JSONRPC error mapping (#414)
Final Error Mapping (JSON-RPC 2.0 Compliant): - `-32700`: Parse error (invalid JSON) - `-32600`: Invalid Request (missing/invalid jsonrpc, invalid id type, oversized payload) - `-32601`: Method not found (unknown method names) - `-32602`: Invalid params (valid method, invalid parameters) -32603: Internal error (unexpected server exceptions) - `-32000` to `-32099`: Server-defined errors (A2A-specific, pass through unchanged) Fixes #413 🦕 --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Holt Skinner <[email protected]> Co-authored-by: Holt Skinner <[email protected]>
1 parent 3ffae8f commit d2e869f

File tree

3 files changed

+107
-16
lines changed

3 files changed

+107
-16
lines changed

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

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@
2828
GetTaskPushNotificationConfigRequest,
2929
GetTaskRequest,
3030
InternalError,
31+
InvalidParamsError,
3132
InvalidRequestError,
3233
JSONParseError,
3334
JSONRPCError,
3435
JSONRPCErrorResponse,
3536
JSONRPCRequest,
3637
JSONRPCResponse,
3738
ListTaskPushNotificationConfigRequest,
39+
MethodNotFoundError,
3840
SendMessageRequest,
3941
SendStreamingMessageRequest,
4042
SendStreamingMessageResponse,
@@ -89,6 +91,8 @@
8991
Response = Any
9092
HTTP_413_REQUEST_ENTITY_TOO_LARGE = Any
9193

94+
MAX_CONTENT_LENGTH = 1_000_000
95+
9296

9397
class StarletteUserProxy(A2AUser):
9498
"""Adapts the Starlette User class to the A2A user representation."""
@@ -151,6 +155,25 @@ class JSONRPCApplication(ABC):
151155
(SSE).
152156
"""
153157

158+
# Method-to-model mapping for centralized routing
159+
A2ARequestModel = (
160+
SendMessageRequest
161+
| SendStreamingMessageRequest
162+
| GetTaskRequest
163+
| CancelTaskRequest
164+
| SetTaskPushNotificationConfigRequest
165+
| GetTaskPushNotificationConfigRequest
166+
| ListTaskPushNotificationConfigRequest
167+
| DeleteTaskPushNotificationConfigRequest
168+
| TaskResubscriptionRequest
169+
| GetAuthenticatedExtendedCardRequest
170+
)
171+
172+
METHOD_TO_MODEL: dict[str, type[A2ARequestModel]] = {
173+
model.model_fields['method'].default: model
174+
for model in A2ARequestModel.__args__
175+
}
176+
154177
def __init__( # noqa: PLR0913
155178
self,
156179
agent_card: AgentCard,
@@ -271,17 +294,60 @@ async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911
271294
body = await request.json()
272295
if isinstance(body, dict):
273296
request_id = body.get('id')
297+
# Ensure request_id is valid for JSON-RPC response (str/int/None only)
298+
if request_id is not None and not isinstance(
299+
request_id, str | int
300+
):
301+
request_id = None
302+
# Treat very large payloads as invalid request (-32600) before routing
303+
with contextlib.suppress(Exception):
304+
content_length = int(request.headers.get('content-length', '0'))
305+
if content_length and content_length > MAX_CONTENT_LENGTH:
306+
return self._generate_error_response(
307+
request_id,
308+
A2AError(
309+
root=InvalidRequestError(
310+
message='Payload too large'
311+
)
312+
),
313+
)
314+
logger.debug('Request body: %s', body)
315+
# 1) Validate base JSON-RPC structure only (-32600 on failure)
316+
try:
317+
base_request = JSONRPCRequest.model_validate(body)
318+
except ValidationError as e:
319+
logger.exception('Failed to validate base JSON-RPC request')
320+
return self._generate_error_response(
321+
request_id,
322+
A2AError(
323+
root=InvalidRequestError(data=json.loads(e.json()))
324+
),
325+
)
274326

275-
# First, validate the basic JSON-RPC structure. This is crucial
276-
# because the A2ARequest model is a discriminated union where some
277-
# request types have default values for the 'method' field
278-
JSONRPCRequest.model_validate(body)
327+
# 2) Route by method name; unknown -> -32601, known -> validate params (-32602 on failure)
328+
method = base_request.method
279329

280-
a2a_request = A2ARequest.model_validate(body)
330+
model_class = self.METHOD_TO_MODEL.get(method)
331+
if not model_class:
332+
return self._generate_error_response(
333+
request_id, A2AError(root=MethodNotFoundError())
334+
)
335+
try:
336+
specific_request = model_class.model_validate(body)
337+
except ValidationError as e:
338+
logger.exception('Failed to validate base JSON-RPC request')
339+
return self._generate_error_response(
340+
request_id,
341+
A2AError(
342+
root=InvalidParamsError(data=json.loads(e.json()))
343+
),
344+
)
281345

346+
# 3) Build call context and wrap the request for downstream handling
282347
call_context = self._context_builder.build(request)
283348

284-
request_id = a2a_request.root.id
349+
request_id = specific_request.id
350+
a2a_request = A2ARequest(root=specific_request)
285351
request_obj = a2a_request.root
286352

287353
if isinstance(
@@ -305,12 +371,6 @@ async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911
305371
return self._generate_error_response(
306372
None, A2AError(root=JSONParseError(message=str(e)))
307373
)
308-
except ValidationError as e:
309-
traceback.print_exc()
310-
return self._generate_error_response(
311-
request_id,
312-
A2AError(root=InvalidRequestError(data=json.loads(e.json()))),
313-
)
314374
except HTTPException as e:
315375
if e.status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE:
316376
return self._generate_error_response(

src/a2a/server/request_handlers/default_request_handler.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
MessageSendParams,
3737
Task,
3838
TaskIdParams,
39+
TaskNotCancelableError,
3940
TaskNotFoundError,
4041
TaskPushNotificationConfig,
4142
TaskQueryParams,
@@ -111,6 +112,26 @@ async def on_get_task(
111112
task: Task | None = await self.task_store.get(params.id)
112113
if not task:
113114
raise ServerError(error=TaskNotFoundError())
115+
116+
# Apply historyLength parameter if specified
117+
if params.history_length is not None and task.history:
118+
# Limit history to the most recent N messages
119+
limited_history = (
120+
task.history[-params.history_length :]
121+
if params.history_length > 0
122+
else []
123+
)
124+
# Create a new task instance with limited history
125+
task = Task(
126+
id=task.id,
127+
context_id=task.context_id,
128+
status=task.status,
129+
artifacts=task.artifacts,
130+
history=limited_history,
131+
metadata=task.metadata,
132+
kind=task.kind,
133+
)
134+
114135
return task
115136

116137
async def on_cancel_task(
@@ -124,6 +145,14 @@ async def on_cancel_task(
124145
if not task:
125146
raise ServerError(error=TaskNotFoundError())
126147

148+
# Check if task is in a non-cancelable state (completed, canceled, failed, rejected)
149+
if task.status.state in TERMINAL_TASK_STATES:
150+
raise ServerError(
151+
error=TaskNotCancelableError(
152+
message=f'Task cannot be canceled - current state: {task.status.state}'
153+
)
154+
)
155+
127156
task_manager = TaskManager(
128157
task_id=task.id,
129158
context_id=task.context_id,

tests/server/test_integration.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
Artifact,
3030
DataPart,
3131
InternalError,
32+
InvalidParamsError,
3233
InvalidRequestError,
3334
JSONParseError,
3435
Message,
36+
MethodNotFoundError,
3537
Part,
3638
PushNotificationConfig,
3739
Role,
@@ -837,7 +839,7 @@ def test_invalid_request_structure(client: TestClient):
837839
response = client.post(
838840
'/',
839841
json={
840-
# Missing required fields
842+
'jsonrpc': 'aaaa', # Missing or wrong required fields
841843
'id': '123',
842844
'method': 'foo/bar',
843845
},
@@ -976,7 +978,7 @@ def test_unknown_method(client: TestClient):
976978
data = response.json()
977979
assert 'error' in data
978980
# This should produce an UnsupportedOperationError error code
979-
assert data['error']['code'] == InvalidRequestError().code
981+
assert data['error']['code'] == MethodNotFoundError().code
980982

981983

982984
def test_validation_error(client: TestClient):
@@ -987,7 +989,7 @@ def test_validation_error(client: TestClient):
987989
json={
988990
'jsonrpc': '2.0',
989991
'id': '123',
990-
'method': 'messages/send',
992+
'method': 'message/send',
991993
'params': {
992994
'message': {
993995
# Missing required fields
@@ -999,7 +1001,7 @@ def test_validation_error(client: TestClient):
9991001
assert response.status_code == 200
10001002
data = response.json()
10011003
assert 'error' in data
1002-
assert data['error']['code'] == InvalidRequestError().code
1004+
assert data['error']['code'] == InvalidParamsError().code
10031005

10041006

10051007
def test_unhandled_exception(client: TestClient, handler: mock.AsyncMock):

0 commit comments

Comments
 (0)