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 ,
8991 Response = Any
9092 HTTP_413_REQUEST_ENTITY_TOO_LARGE = Any
9193
94+ MAX_CONTENT_LENGTH = 1_000_000
95+
9296
9397class 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 ,
@@ -233,9 +256,13 @@ def _generate_error_response(
233256 )
234257 logger .log (
235258 log_level ,
236- f'Request Error (ID: { request_id } ): '
237- f"Code={ error_resp .error .code } , Message='{ error_resp .error .message } '"
238- f'{ ", Data=" + str (error_resp .error .data ) if error_resp .error .data else "" } ' ,
259+ "Request Error (ID: %s): Code=%s, Message='%s'%s" ,
260+ request_id ,
261+ error_resp .error .code ,
262+ error_resp .error .message ,
263+ ', Data=' + str (error_resp .error .data )
264+ if error_resp .error .data
265+ else '' ,
239266 )
240267 return JSONResponse (
241268 error_resp .model_dump (mode = 'json' , exclude_none = True ),
@@ -267,17 +294,60 @@ async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911
267294 body = await request .json ()
268295 if isinstance (body , dict ):
269296 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+ )
270326
271- # First, validate the basic JSON-RPC structure. This is crucial
272- # because the A2ARequest model is a discriminated union where some
273- # request types have default values for the 'method' field
274- JSONRPCRequest .model_validate (body )
327+ # 2) Route by method name; unknown -> -32601, known -> validate params (-32602 on failure)
328+ method = base_request .method
275329
276- 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+ )
277345
346+ # 3) Build call context and wrap the request for downstream handling
278347 call_context = self ._context_builder .build (request )
279348
280- request_id = a2a_request .root .id
349+ request_id = specific_request .id
350+ a2a_request = A2ARequest (root = specific_request )
281351 request_obj = a2a_request .root
282352
283353 if isinstance (
@@ -301,12 +371,6 @@ async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911
301371 return self ._generate_error_response (
302372 None , A2AError (root = JSONParseError (message = str (e )))
303373 )
304- except ValidationError as e :
305- traceback .print_exc ()
306- return self ._generate_error_response (
307- request_id ,
308- A2AError (root = InvalidRequestError (data = json .loads (e .json ()))),
309- )
310374 except HTTPException as e :
311375 if e .status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE :
312376 return self ._generate_error_response (
@@ -422,7 +486,7 @@ async def _process_non_streaming_request(
422486 )
423487 case _:
424488 logger .error (
425- f 'Unhandled validated request type: { type (request_obj )} '
489+ 'Unhandled validated request type: %s' , type (request_obj )
426490 )
427491 error = UnsupportedOperationError (
428492 message = f'Request type { type (request_obj ).__name__ } is unknown.'
@@ -497,8 +561,10 @@ async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
497561 """
498562 if request .url .path == PREV_AGENT_CARD_WELL_KNOWN_PATH :
499563 logger .warning (
500- f"Deprecated agent card endpoint '{ PREV_AGENT_CARD_WELL_KNOWN_PATH } ' accessed. "
501- f"Please use '{ AGENT_CARD_WELL_KNOWN_PATH } ' instead. This endpoint will be removed in a future version."
564+ "Deprecated agent card endpoint '%s' accessed. "
565+ "Please use '%s' instead. This endpoint will be removed in a future version." ,
566+ PREV_AGENT_CARD_WELL_KNOWN_PATH ,
567+ AGENT_CARD_WELL_KNOWN_PATH ,
502568 )
503569
504570 card_to_serve = self .agent_card
0 commit comments