1use std::sync::Arc;
4
5use a2a::*;
6use a2a_pb::protojson_conv::{self, ProtoJsonPayload};
7use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
8use futures::{StreamExt, stream::BoxStream};
9use serde_json::Value;
10
11use crate::handler::RequestHandler;
12use crate::middleware::ServiceParams;
13use crate::push_config_compat::json_value as compat_json_value;
14use crate::sse;
15
16pub struct JsonRpcState<H: RequestHandler> {
18 pub handler: Arc<H>,
19}
20
21impl<H: RequestHandler> Clone for JsonRpcState<H> {
22 fn clone(&self) -> Self {
23 JsonRpcState {
24 handler: self.handler.clone(),
25 }
26 }
27}
28
29pub fn jsonrpc_router<H: RequestHandler>(handler: Arc<H>) -> axum::Router {
34 let state = JsonRpcState { handler };
35 axum::Router::new()
36 .route("/", axum::routing::post(handle_jsonrpc::<H>))
37 .with_state(state)
38}
39
40async fn handle_jsonrpc<H: RequestHandler>(
41 State(state): State<JsonRpcState<H>>,
42 Json(request): Json<JsonRpcRequest>,
43) -> impl IntoResponse {
44 let params = ServiceParams::new();
45 let id = request.id.clone();
46 let method = request.method.as_str();
47
48 if request.jsonrpc != "2.0" {
49 return error_response(id, A2AError::invalid_request("invalid jsonrpc version"));
50 }
51
52 if methods::is_streaming(method) {
53 return handle_streaming_request(&state, ¶ms, &request).await;
54 }
55
56 handle_unary_request(&state, ¶ms, &request).await
57}
58
59async fn handle_unary_request<H: RequestHandler>(
60 state: &JsonRpcState<H>,
61 params: &ServiceParams,
62 request: &JsonRpcRequest,
63) -> axum::response::Response {
64 let id = request.id.clone();
65 let raw_params = request.params.clone().unwrap_or(Value::Null);
66
67 let result: Result<Value, A2AError> = match request.method.as_str() {
68 methods::SEND_MESSAGE => match protojson_conv::from_value::<SendMessageRequest>(raw_params)
69 {
70 Ok(req) => state
71 .handler
72 .send_message(params, req)
73 .await
74 .and_then(|r| protojson_value(&r)),
75 Err(e) => Err(parse_error(e)),
76 },
77 methods::GET_TASK => match protojson_conv::from_value::<GetTaskRequest>(raw_params) {
78 Ok(req) => state
79 .handler
80 .get_task(params, req)
81 .await
82 .and_then(|r| protojson_value(&r)),
83 Err(e) => Err(parse_error(e)),
84 },
85 methods::LIST_TASKS => match protojson_conv::from_value::<ListTasksRequest>(raw_params) {
86 Ok(req) => state
87 .handler
88 .list_tasks(params, req)
89 .await
90 .and_then(|r| protojson_value(&r)),
91 Err(e) => Err(parse_error(e)),
92 },
93 methods::CANCEL_TASK => match protojson_conv::from_value::<CancelTaskRequest>(raw_params) {
94 Ok(req) => state
95 .handler
96 .cancel_task(params, req)
97 .await
98 .and_then(|r| protojson_value(&r)),
99 Err(e) => Err(parse_error(e)),
100 },
101 methods::CREATE_PUSH_CONFIG => match parse_create_push_config_request(raw_params) {
102 Ok(req) => state
103 .handler
104 .create_push_config(params, req)
105 .await
106 .and_then(|r| compat_json_value(&r)),
107 Err(e) => Err(parse_error(e)),
108 },
109 methods::GET_PUSH_CONFIG => {
110 match protojson_conv::from_value::<GetTaskPushNotificationConfigRequest>(raw_params) {
111 Ok(req) => state
112 .handler
113 .get_push_config(params, req)
114 .await
115 .and_then(|r| compat_json_value(&r)),
116 Err(e) => Err(parse_error(e)),
117 }
118 }
119 methods::LIST_PUSH_CONFIGS => {
120 match protojson_conv::from_value::<ListTaskPushNotificationConfigsRequest>(raw_params) {
121 Ok(req) => state
122 .handler
123 .list_push_configs(params, req)
124 .await
125 .and_then(|r| compat_json_value(&r.configs)),
126 Err(e) => Err(parse_error(e)),
127 }
128 }
129 methods::DELETE_PUSH_CONFIG => {
130 match protojson_conv::from_value::<DeleteTaskPushNotificationConfigRequest>(raw_params)
131 {
132 Ok(req) => state
133 .handler
134 .delete_push_config(params, req)
135 .await
136 .map(|_| Value::Null),
137 Err(e) => Err(parse_error(e)),
138 }
139 }
140 methods::GET_EXTENDED_AGENT_CARD => {
141 match protojson_conv::from_value::<GetExtendedAgentCardRequest>(raw_params) {
142 Ok(req) => state
143 .handler
144 .get_extended_agent_card(params, req)
145 .await
146 .and_then(|r| protojson_value(&r)),
147 Err(e) => Err(parse_error(e)),
148 }
149 }
150 "" => Err(A2AError::invalid_request("method is required")),
151 _ => Err(A2AError::method_not_found(&request.method)),
152 };
153
154 match result {
155 Ok(value) => {
156 let resp = JsonRpcResponse::success(id, value);
157 Json(resp).into_response()
158 }
159 Err(e) => error_response(id, e),
160 }
161}
162
163async fn handle_streaming_request<H: RequestHandler>(
164 state: &JsonRpcState<H>,
165 params: &ServiceParams,
166 request: &JsonRpcRequest,
167) -> axum::response::Response {
168 let id = request.id.clone();
169 let raw_params = request.params.clone().unwrap_or(Value::Null);
170
171 match request.method.as_str() {
172 methods::SEND_STREAMING_MESSAGE => {
173 match protojson_conv::from_value::<SendMessageRequest>(raw_params) {
174 Ok(req) => match state.handler.send_streaming_message(params, req).await {
175 Ok(stream) => {
176 sse::sse_jsonrpc_stream(id, protojson_stream(stream)).into_response()
177 }
178 Err(e) => error_response(id, e),
179 },
180 Err(e) => error_response(id, parse_error(e)),
181 }
182 }
183 methods::SUBSCRIBE_TO_TASK => {
184 match protojson_conv::from_value::<SubscribeToTaskRequest>(raw_params) {
185 Ok(req) => match state.handler.subscribe_to_task(params, req).await {
186 Ok(stream) => {
187 sse::sse_jsonrpc_stream(id, protojson_stream(stream)).into_response()
188 }
189 Err(e) => error_response(id, e),
190 },
191 Err(e) => error_response(id, parse_error(e)),
192 }
193 }
194 _ => error_response(id, A2AError::method_not_found(&request.method)),
195 }
196}
197
198fn error_response(id: JsonRpcId, err: A2AError) -> axum::response::Response {
199 let resp = JsonRpcResponse::error(id, err.to_jsonrpc_error());
200 (StatusCode::OK, Json(resp)).into_response()
201}
202
203fn protojson_value<T: ProtoJsonPayload>(value: &T) -> Result<Value, A2AError> {
204 protojson_conv::to_value(value)
205 .map_err(|e| A2AError::internal(format!("failed to serialize ProtoJSON payload: {e}")))
206}
207
208fn protojson_stream(
209 stream: BoxStream<'static, Result<StreamResponse, A2AError>>,
210) -> BoxStream<'static, Result<Value, A2AError>> {
211 Box::pin(stream.map(|item| {
212 item.and_then(|value| {
213 protojson_conv::to_value(&value).map_err(|e| {
214 A2AError::internal(format!("failed to serialize ProtoJSON stream payload: {e}"))
215 })
216 })
217 }))
218}
219
220fn parse_create_push_config_request(
221 raw_params: Value,
222) -> Result<CreateTaskPushNotificationConfigRequest, String> {
223 match protojson_conv::from_value::<CreateTaskPushNotificationConfigRequest>(raw_params.clone())
224 {
225 Ok(req) => Ok(req),
226 Err(protojson_error) => serde_json::from_value::<CreateTaskPushNotificationConfigRequest>(
227 raw_params,
228 )
229 .map_err(|serde_error| {
230 format!("{protojson_error}; nested request parse failed: {serde_error}")
231 }),
232 }
233}
234
235fn parse_error(e: impl std::fmt::Display) -> A2AError {
236 A2AError {
237 code: error_code::PARSE_ERROR,
238 message: format!("invalid params: {e}"),
239 details: None,
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use crate::InMemoryPushConfigStore;
247 use crate::executor::ExecutorContext;
248 use crate::handler::DefaultRequestHandler;
249 use crate::task_store::InMemoryTaskStore;
250 use a2a_pb::protojson_conv;
251 use axum::body::Body;
252 use axum::http::Request;
253 use futures::stream::BoxStream;
254 use http_body_util::BodyExt;
255 use tower::ServiceExt;
256
257 struct EchoExecutor;
258
259 impl crate::AgentExecutor for EchoExecutor {
260 fn execute(
261 &self,
262 ctx: ExecutorContext,
263 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
264 let task = Task {
265 id: ctx.task_id.clone(),
266 context_id: ctx.context_id.clone(),
267 status: TaskStatus {
268 state: TaskState::Completed,
269 message: ctx.message,
270 timestamp: None,
271 },
272 artifacts: None,
273 history: None,
274 metadata: None,
275 };
276 Box::pin(futures::stream::once(async move {
277 Ok(StreamResponse::Task(task))
278 }))
279 }
280
281 fn cancel(
282 &self,
283 ctx: ExecutorContext,
284 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
285 let task = Task {
286 id: ctx.task_id.clone(),
287 context_id: ctx.context_id.clone(),
288 status: TaskStatus {
289 state: TaskState::Canceled,
290 message: None,
291 timestamp: None,
292 },
293 artifacts: None,
294 history: None,
295 metadata: None,
296 };
297 Box::pin(futures::stream::once(async move {
298 Ok(StreamResponse::Task(task))
299 }))
300 }
301 }
302
303 fn make_app() -> axum::Router {
304 let handler = Arc::new(DefaultRequestHandler::new(
305 EchoExecutor,
306 InMemoryTaskStore::new(),
307 ));
308 jsonrpc_router(handler)
309 }
310
311 fn make_push_app() -> axum::Router {
312 let handler = Arc::new(
313 DefaultRequestHandler::new(EchoExecutor, InMemoryTaskStore::new())
314 .with_push_config_store(InMemoryPushConfigStore::new()),
315 );
316 jsonrpc_router(handler)
317 }
318
319 async fn post_jsonrpc(app: axum::Router, method: &str, params: Value) -> JsonRpcResponse {
320 let rpc = JsonRpcRequest::new(JsonRpcId::Number(1), method, Some(params));
321 let body = serde_json::to_string(&rpc).unwrap();
322 let req = Request::builder()
323 .uri("/")
324 .method("POST")
325 .header("content-type", "application/json")
326 .body(Body::from(body))
327 .unwrap();
328 let resp = app.oneshot(req).await.unwrap();
329 let body = resp.into_body().collect().await.unwrap().to_bytes();
330 serde_json::from_slice(&body).unwrap()
331 }
332
333 #[tokio::test]
334 async fn test_send_message() {
335 let app = make_app();
336 let params = serde_json::json!({
337 "message": {
338 "messageId": "m1",
339 "role": "ROLE_USER",
340 "parts": [{"text": "hello"}]
341 }
342 });
343 let resp = post_jsonrpc(app, methods::SEND_MESSAGE, params).await;
344 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
345 let result =
346 protojson_conv::from_value::<SendMessageResponse>(resp.result.unwrap()).unwrap();
347 assert!(matches!(result, SendMessageResponse::Task(_)));
348 }
349
350 #[tokio::test]
351 async fn test_send_message_legacy_method_rejected() {
352 let app = make_app();
353 let params = serde_json::json!({
354 "message": {
355 "messageId": "m1",
356 "role": "ROLE_USER",
357 "parts": [{"text": "hello"}]
358 }
359 });
360 let resp = post_jsonrpc(app, "message.send", params).await;
361 assert!(resp.error.is_some(), "unexpected result: {:?}", resp.result);
362 assert_eq!(resp.error.unwrap().code, error_code::METHOD_NOT_FOUND);
363 }
364
365 #[tokio::test]
366 async fn test_get_task_not_found() {
367 let app = make_app();
368 let params = serde_json::json!({"id": "nonexistent"});
369 let resp = post_jsonrpc(app, methods::GET_TASK, params).await;
370 assert!(resp.error.is_some());
371 assert_eq!(resp.error.unwrap().code, error_code::TASK_NOT_FOUND);
372 }
373
374 #[tokio::test]
375 async fn test_invalid_method() {
376 let app = make_app();
377 let resp = post_jsonrpc(app, "unknown.method", Value::Null).await;
378 assert!(resp.error.is_some());
379 assert_eq!(resp.error.unwrap().code, error_code::METHOD_NOT_FOUND);
380 }
381
382 #[tokio::test]
383 async fn test_empty_method() {
384 let app = make_app();
385 let resp = post_jsonrpc(app, "", Value::Null).await;
386 assert!(resp.error.is_some());
387 }
388
389 #[tokio::test]
390 async fn test_invalid_jsonrpc_version() {
391 let app = make_app();
392 let rpc = serde_json::json!({
393 "jsonrpc": "1.0",
394 "id": 1,
395 "method": methods::SEND_MESSAGE,
396 "params": {}
397 });
398 let body = serde_json::to_string(&rpc).unwrap();
399 let req = Request::builder()
400 .uri("/")
401 .method("POST")
402 .header("content-type", "application/json")
403 .body(Body::from(body))
404 .unwrap();
405 let resp = app.oneshot(req).await.unwrap();
406 let body = resp.into_body().collect().await.unwrap().to_bytes();
407 let rpc_resp: JsonRpcResponse = serde_json::from_slice(&body).unwrap();
408 assert!(rpc_resp.error.is_some());
409 }
410
411 #[tokio::test]
412 async fn test_invalid_params() {
413 let app = make_app();
414 let params = serde_json::json!({"bogus": true});
415 let resp = post_jsonrpc(app, methods::SEND_MESSAGE, params).await;
416 assert!(resp.error.is_some());
417 assert_eq!(resp.error.unwrap().code, error_code::PARSE_ERROR);
418 }
419
420 #[tokio::test]
421 async fn test_list_tasks() {
422 let app = make_app();
423 let params = serde_json::json!({});
424 let resp = post_jsonrpc(app, methods::LIST_TASKS, params).await;
425 let result = protojson_conv::from_value::<ListTasksResponse>(resp.result.unwrap()).unwrap();
426 assert!(result.tasks.is_empty());
427 }
428
429 #[tokio::test]
430 async fn test_cancel_task_not_found() {
431 let app = make_app();
432 let params = serde_json::json!({"id": "nonexistent"});
433 let resp = post_jsonrpc(app, methods::CANCEL_TASK, params).await;
434 assert!(resp.error.is_some());
435 }
436
437 #[tokio::test]
438 async fn test_create_push_config() {
439 let app = make_push_app();
440 let params = serde_json::json!({
441 "taskId": "t1",
442 "id": "cfg1",
443 "url": "http://example.com/callback"
444 });
445 let resp = post_jsonrpc(app, methods::CREATE_PUSH_CONFIG, params).await;
446 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
447 let result =
448 serde_json::from_value::<TaskPushNotificationConfig>(resp.result.unwrap()).unwrap();
449 assert_eq!(result.task_id, "t1");
450 assert_eq!(result.config.id.as_deref(), Some("cfg1"));
451 assert_eq!(result.config.url, "http://example.com/callback");
452 }
453
454 #[tokio::test]
455 async fn test_create_push_config_accepts_nested_config_object() {
456 let app = make_push_app();
457 let params = serde_json::json!({
458 "taskId": "t1",
459 "config": {
460 "id": "cfg1",
461 "url": "http://example.com/callback"
462 }
463 });
464 let resp = post_jsonrpc(app, methods::CREATE_PUSH_CONFIG, params).await;
465 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
466 let result =
467 serde_json::from_value::<TaskPushNotificationConfig>(resp.result.unwrap()).unwrap();
468 assert_eq!(result.task_id, "t1");
469 assert_eq!(result.config.id.as_deref(), Some("cfg1"));
470 assert_eq!(result.config.url, "http://example.com/callback");
471 }
472
473 #[tokio::test]
474 async fn test_get_push_config() {
475 let app = make_app();
476 let params = serde_json::json!({
477 "taskId": "t1",
478 "id": "cfg1"
479 });
480 let resp = post_jsonrpc(app, methods::GET_PUSH_CONFIG, params).await;
481 assert!(resp.error.is_some() || resp.result.is_some());
482 }
483
484 #[tokio::test]
485 async fn test_list_push_configs() {
486 let app = make_push_app();
487 let params = serde_json::json!({
488 "taskId": "t1"
489 });
490 let resp = post_jsonrpc(app, methods::LIST_PUSH_CONFIGS, params).await;
491 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
492 let result =
493 serde_json::from_value::<Vec<TaskPushNotificationConfig>>(resp.result.unwrap())
494 .unwrap();
495 assert!(result.is_empty());
496 }
497
498 #[tokio::test]
499 async fn test_delete_push_config() {
500 let app = make_app();
501 let params = serde_json::json!({
502 "taskId": "t1",
503 "id": "cfg1"
504 });
505 let resp = post_jsonrpc(app, methods::DELETE_PUSH_CONFIG, params).await;
506 assert!(resp.error.is_some() || resp.result.is_some());
507 }
508
509 #[tokio::test]
510 async fn test_get_extended_agent_card() {
511 let app = make_app();
512 let params = serde_json::json!({});
513 let resp = post_jsonrpc(app, methods::GET_EXTENDED_AGENT_CARD, params).await;
514 assert!(resp.error.is_some() || resp.result.is_some());
516 }
517
518 #[tokio::test]
519 async fn test_streaming_send_message() {
520 let app = make_app();
521 let body = serde_json::json!({
522 "message": {
523 "messageId": "m1",
524 "role": "ROLE_USER",
525 "parts": [{"text": "hello"}]
526 }
527 });
528 let rpc = JsonRpcRequest::new(
529 JsonRpcId::Number(1),
530 methods::SEND_STREAMING_MESSAGE,
531 Some(body),
532 );
533 let req = Request::builder()
534 .uri("/")
535 .method("POST")
536 .header("content-type", "application/json")
537 .header("accept", "text/event-stream")
538 .body(Body::from(serde_json::to_string(&rpc).unwrap()))
539 .unwrap();
540 let resp = app.oneshot(req).await.unwrap();
541 assert_eq!(resp.status(), StatusCode::OK);
543 }
544
545 #[tokio::test]
546 async fn test_streaming_subscribe_to_task() {
547 let app = make_app();
548 let rpc = JsonRpcRequest::new(
549 JsonRpcId::Number(1),
550 methods::SUBSCRIBE_TO_TASK,
551 Some(serde_json::json!({"id": "t1"})),
552 );
553 let req = Request::builder()
554 .uri("/")
555 .method("POST")
556 .header("content-type", "application/json")
557 .body(Body::from(serde_json::to_string(&rpc).unwrap()))
558 .unwrap();
559 let resp = app.oneshot(req).await.unwrap();
560 assert_eq!(resp.status(), StatusCode::OK);
562 }
563
564 #[tokio::test]
565 async fn test_streaming_send_message_legacy_method_rejected() {
566 let app = make_app();
567 let body = serde_json::json!({
568 "message": {
569 "messageId": "m1",
570 "role": "ROLE_USER",
571 "parts": [{"text": "hello"}]
572 }
573 });
574 let rpc = JsonRpcRequest::new(JsonRpcId::Number(1), "message.stream", Some(body));
575 let req = Request::builder()
576 .uri("/")
577 .method("POST")
578 .header("content-type", "application/json")
579 .header("accept", "text/event-stream")
580 .body(Body::from(serde_json::to_string(&rpc).unwrap()))
581 .unwrap();
582 let resp = app.oneshot(req).await.unwrap();
583 let body = resp.into_body().collect().await.unwrap().to_bytes();
584 let rpc_resp: JsonRpcResponse = serde_json::from_slice(&body).unwrap();
585 assert!(
586 rpc_resp.error.is_some(),
587 "unexpected result: {:?}",
588 rpc_resp.result
589 );
590 assert_eq!(rpc_resp.error.unwrap().code, error_code::METHOD_NOT_FOUND);
591 }
592
593 #[tokio::test]
594 async fn test_streaming_invalid_method() {
595 let app = make_app();
596 let rpc = serde_json::json!({
597 "jsonrpc": "2.0",
598 "id": 1,
599 "method": "tasks.subscribe",
600 "params": {"bogus": true}
601 });
602 let req = Request::builder()
603 .uri("/")
604 .method("POST")
605 .header("content-type", "application/json")
606 .body(Body::from(serde_json::to_string(&rpc).unwrap()))
607 .unwrap();
608 let resp = app.oneshot(req).await.unwrap();
609 assert_eq!(resp.status(), StatusCode::OK);
610 }
611}