1use std::sync::Arc;
4
5use a2a::*;
6use a2a_pb::protojson_conv::{self, ProtoJsonPayload};
7use axum::{
8 Json,
9 extract::{Path, Query, State},
10 http::{HeaderMap, StatusCode},
11 response::IntoResponse,
12};
13use chrono::{DateTime, Utc};
14use futures::{StreamExt, stream::BoxStream};
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use std::collections::HashMap;
18
19use crate::handler::RequestHandler;
20use crate::middleware::{ServiceParams, extract_service_params};
21use crate::sse;
22
23const REST_SEND_MESSAGE_PATH: &str = "/message:send";
24const REST_SEND_MESSAGE_LEGACY_PATH: &str = "/message/send";
25const REST_STREAM_MESSAGE_PATH: &str = "/message:stream";
26const REST_STREAM_MESSAGE_LEGACY_PATH: &str = "/message/stream";
27const REST_EXTENDED_AGENT_CARD_PATH: &str = "/extendedAgentCard";
28const REST_EXTENDED_AGENT_CARD_LEGACY_PATH: &str = "/agent-card/extended";
29const REST_PUSH_CONFIGS_PATH: &str = "/tasks/{id}/pushNotificationConfigs";
30const REST_PUSH_CONFIGS_LEGACY_PATH: &str = "/tasks/{id}/push-configs";
31const REST_PUSH_CONFIG_PATH: &str = "/tasks/{id}/pushNotificationConfigs/{config_id}";
32const REST_PUSH_CONFIG_LEGACY_PATH: &str = "/tasks/{id}/push-configs/{config_id}";
33#[derive(Serialize)]
34struct RestErrorEnvelope {
35 error: RestErrorStatus,
36}
37
38#[derive(Serialize)]
39struct RestErrorStatus {
40 code: u16,
41 status: &'static str,
42 message: String,
43 details: Vec<TypedDetail>,
44}
45
46pub struct RestState<H: RequestHandler> {
48 pub handler: Arc<H>,
49}
50
51impl<H: RequestHandler> Clone for RestState<H> {
52 fn clone(&self) -> Self {
53 RestState {
54 handler: self.handler.clone(),
55 }
56 }
57}
58
59pub fn rest_router<H: RequestHandler>(handler: Arc<H>) -> axum::Router {
61 let state = RestState { handler };
62 axum::Router::new()
63 .route(
64 REST_SEND_MESSAGE_PATH,
65 axum::routing::post(handle_send_message::<H>),
66 )
67 .route(
68 REST_SEND_MESSAGE_LEGACY_PATH,
69 axum::routing::post(handle_send_message::<H>),
70 )
71 .route(
72 REST_STREAM_MESSAGE_PATH,
73 axum::routing::post(handle_stream_message::<H>),
74 )
75 .route(
76 REST_STREAM_MESSAGE_LEGACY_PATH,
77 axum::routing::post(handle_stream_message::<H>),
78 )
79 .route(
80 "/tasks/{id}",
81 axum::routing::get(handle_get_task_or_subscribe::<H>)
82 .post(handle_post_task_action::<H>),
83 )
84 .route("/tasks", axum::routing::get(handle_list_tasks::<H>))
85 .route(
86 "/tasks/{id}/cancel",
87 axum::routing::post(handle_cancel_task_legacy::<H>),
88 )
89 .route(
90 "/tasks/{id}/subscribe",
91 axum::routing::get(handle_subscribe_to_task_legacy::<H>)
92 .post(handle_subscribe_to_task_legacy::<H>),
93 )
94 .route(
95 REST_PUSH_CONFIGS_PATH,
96 axum::routing::post(handle_create_push_config::<H>).get(handle_list_push_configs::<H>),
97 )
98 .route(
99 REST_PUSH_CONFIGS_LEGACY_PATH,
100 axum::routing::post(handle_create_push_config::<H>).get(handle_list_push_configs::<H>),
101 )
102 .route(
103 REST_PUSH_CONFIG_PATH,
104 axum::routing::get(handle_get_push_config::<H>).delete(handle_delete_push_config::<H>),
105 )
106 .route(
107 REST_PUSH_CONFIG_LEGACY_PATH,
108 axum::routing::get(handle_get_push_config::<H>).delete(handle_delete_push_config::<H>),
109 )
110 .route(
111 REST_EXTENDED_AGENT_CARD_PATH,
112 axum::routing::get(handle_get_extended_agent_card::<H>),
113 )
114 .route(
115 REST_EXTENDED_AGENT_CARD_LEGACY_PATH,
116 axum::routing::get(handle_get_extended_agent_card::<H>),
117 )
118 .layer(axum::extract::DefaultBodyLimit::max(
119 crate::jsonrpc::MAX_REQUEST_BODY_BYTES,
120 ))
121 .with_state(state)
122}
123
124async fn handle_send_message<H: RequestHandler>(
125 State(state): State<RestState<H>>,
126 headers: HeaderMap,
127 Json(raw_req): Json<Value>,
128) -> impl IntoResponse {
129 let req = match protojson_conv::from_value::<SendMessageRequest>(raw_req) {
130 Ok(req) => req,
131 Err(e) => return rest_payload_error_response(e),
132 };
133 let params = extract_service_params(&headers);
134 match state.handler.send_message(¶ms, req).await {
135 Ok(resp) => protojson_json_response(&resp),
136 Err(e) => rest_error_response(e),
137 }
138}
139
140async fn handle_stream_message<H: RequestHandler>(
141 State(state): State<RestState<H>>,
142 headers: HeaderMap,
143 Json(raw_req): Json<Value>,
144) -> impl IntoResponse {
145 let req = match protojson_conv::from_value::<SendMessageRequest>(raw_req) {
146 Ok(req) => req,
147 Err(e) => return rest_payload_error_response(e),
148 };
149 let params = extract_service_params(&headers);
150 match state.handler.send_streaming_message(¶ms, req).await {
151 Ok(stream) => sse::sse_from_stream(protojson_stream(stream)).into_response(),
152 Err(e) => rest_error_response(e),
153 }
154}
155
156#[derive(Deserialize)]
157pub struct GetTaskQuery {
158 #[serde(default, rename = "historyLength")]
159 pub history_length: Option<i32>,
160}
161
162async fn handle_get_task_inner<H: RequestHandler>(
163 state: RestState<H>,
164 params: ServiceParams,
165 id: String,
166 query: GetTaskQuery,
167) -> axum::response::Response {
168 let req = GetTaskRequest {
169 id,
170 history_length: query.history_length,
171 tenant: None,
172 };
173 match state.handler.get_task(¶ms, req).await {
174 Ok(task) => protojson_json_response(&task),
175 Err(e) => rest_error_response(e),
176 }
177}
178
179async fn handle_get_task_or_subscribe<H: RequestHandler>(
180 State(state): State<RestState<H>>,
181 Path(id): Path<String>,
182 Query(query): Query<GetTaskQuery>,
183 headers: HeaderMap,
184) -> impl IntoResponse {
185 let params = extract_service_params(&headers);
186 if let Some(task_id) = id.strip_suffix(":subscribe") {
187 return handle_subscribe_to_task_inner(state, params, task_id.to_string()).await;
188 }
189
190 handle_get_task_inner(state, params, id, query).await
191}
192
193#[derive(Deserialize)]
194pub struct ListTasksQuery {
195 #[serde(default, rename = "contextId")]
196 pub context_id: Option<String>,
197 #[serde(default)]
198 pub status: Option<String>,
199 #[serde(default, rename = "pageSize")]
200 pub page_size: Option<i32>,
201 #[serde(default, rename = "pageToken")]
202 pub page_token: Option<String>,
203 #[serde(default, rename = "historyLength")]
204 pub history_length: Option<i32>,
205
206 #[serde(default, rename = "statusTimestampAfter")]
207 pub status_timestamp_after: Option<DateTime<Utc>>,
208
209 #[serde(default, rename = "includeArtifacts")]
210 pub include_artifacts: Option<bool>,
211}
212
213async fn handle_list_tasks<H: RequestHandler>(
214 State(state): State<RestState<H>>,
215 Query(query): Query<ListTasksQuery>,
216 headers: HeaderMap,
217) -> impl IntoResponse {
218 let params = extract_service_params(&headers);
219 let status = match query.status {
220 Some(s) => match serde_json::from_value::<TaskState>(serde_json::Value::String(s)) {
221 Ok(state) => Some(state),
222 Err(_) => {
223 return rest_error_response(A2AError::invalid_params("invalid status filter"));
224 }
225 },
226 None => None,
227 };
228 let req = ListTasksRequest {
229 context_id: query.context_id,
230 status,
231 page_size: query.page_size,
232 page_token: query.page_token,
233 history_length: query.history_length,
234 status_timestamp_after: query.status_timestamp_after,
235 include_artifacts: query.include_artifacts,
236 tenant: None,
237 };
238 match state.handler.list_tasks(¶ms, req).await {
239 Ok(resp) => protojson_json_response(&resp),
240 Err(e) => rest_error_response(e),
241 }
242}
243
244async fn handle_cancel_task_inner<H: RequestHandler>(
245 state: RestState<H>,
246 params: ServiceParams,
247 id: String,
248) -> axum::response::Response {
249 let req = CancelTaskRequest {
250 id,
251 metadata: None,
252 tenant: None,
253 };
254 match state.handler.cancel_task(¶ms, req).await {
255 Ok(task) => protojson_json_response(&task),
256 Err(e) => rest_error_response(e),
257 }
258}
259
260async fn handle_post_task_action<H: RequestHandler>(
261 State(state): State<RestState<H>>,
262 Path(id): Path<String>,
263 headers: HeaderMap,
264) -> impl IntoResponse {
265 let params = extract_service_params(&headers);
266 if let Some(task_id) = id.strip_suffix(":cancel") {
267 return handle_cancel_task_inner(state, params, task_id.to_string()).await;
268 }
269 if let Some(task_id) = id.strip_suffix(":subscribe") {
270 return handle_subscribe_to_task_inner(state, params, task_id.to_string()).await;
271 }
272
273 rest_error_response(A2AError::invalid_request("unsupported task action"))
274}
275
276async fn handle_cancel_task_legacy<H: RequestHandler>(
277 State(state): State<RestState<H>>,
278 Path(id): Path<String>,
279 headers: HeaderMap,
280) -> impl IntoResponse {
281 let params = extract_service_params(&headers);
282 handle_cancel_task_inner(state, params, id).await
283}
284
285async fn handle_subscribe_to_task_inner<H: RequestHandler>(
286 state: RestState<H>,
287 params: ServiceParams,
288 id: String,
289) -> axum::response::Response {
290 let req = SubscribeToTaskRequest { id, tenant: None };
291 match state.handler.subscribe_to_task(¶ms, req).await {
292 Ok(stream) => sse::sse_from_stream(protojson_stream(stream)).into_response(),
293 Err(e) => rest_error_response(e),
294 }
295}
296
297async fn handle_subscribe_to_task_legacy<H: RequestHandler>(
298 State(state): State<RestState<H>>,
299 Path(id): Path<String>,
300 headers: HeaderMap,
301) -> impl IntoResponse {
302 let params = extract_service_params(&headers);
303 handle_subscribe_to_task_inner(state, params, id).await
304}
305
306#[derive(Deserialize)]
307pub struct ListPushConfigsQuery {
308 #[serde(default, rename = "pageSize")]
309 pub page_size: Option<i32>,
310
311 #[serde(default, rename = "pageToken")]
312 pub page_token: Option<String>,
313}
314
315async fn handle_create_push_config<H: RequestHandler>(
316 State(state): State<RestState<H>>,
317 Path(id): Path<String>,
318 headers: HeaderMap,
319 Json(raw_config): Json<Value>,
320) -> impl IntoResponse {
321 let req = match parse_create_push_config_request(id, raw_config) {
322 Ok(req) => req,
323 Err(e) => return rest_payload_error_response(e),
324 };
325 let params = extract_service_params(&headers);
326 match state.handler.create_push_config(¶ms, req).await {
327 Ok(resp) => protojson_json_response(&resp),
328 Err(e) => rest_error_response(e),
329 }
330}
331
332async fn handle_get_push_config<H: RequestHandler>(
333 State(state): State<RestState<H>>,
334 Path((id, config_id)): Path<(String, String)>,
335 headers: HeaderMap,
336) -> impl IntoResponse {
337 let params = extract_service_params(&headers);
338 let req = GetTaskPushNotificationConfigRequest {
339 task_id: id,
340 id: config_id,
341 tenant: None,
342 };
343 match state.handler.get_push_config(¶ms, req).await {
344 Ok(resp) => protojson_json_response(&resp),
345 Err(e) => rest_error_response(e),
346 }
347}
348
349async fn handle_list_push_configs<H: RequestHandler>(
350 State(state): State<RestState<H>>,
351 Path(id): Path<String>,
352 Query(query): Query<ListPushConfigsQuery>,
353 headers: HeaderMap,
354) -> impl IntoResponse {
355 let params = extract_service_params(&headers);
356 let req = ListTaskPushNotificationConfigsRequest {
357 task_id: id,
358 page_size: query.page_size,
359 page_token: query.page_token,
360 tenant: None,
361 };
362 match state.handler.list_push_configs(¶ms, req).await {
363 Ok(resp) => protojson_json_response(&resp),
364 Err(e) => rest_error_response(e),
365 }
366}
367
368async fn handle_delete_push_config<H: RequestHandler>(
369 State(state): State<RestState<H>>,
370 Path((id, config_id)): Path<(String, String)>,
371 headers: HeaderMap,
372) -> impl IntoResponse {
373 let params = extract_service_params(&headers);
374 let req = DeleteTaskPushNotificationConfigRequest {
375 task_id: id,
376 id: config_id,
377 tenant: None,
378 };
379 match state.handler.delete_push_config(¶ms, req).await {
380 Ok(()) => StatusCode::OK.into_response(),
381 Err(e) => rest_error_response(e),
382 }
383}
384
385async fn handle_get_extended_agent_card<H: RequestHandler>(
386 State(state): State<RestState<H>>,
387 headers: HeaderMap,
388) -> impl IntoResponse {
389 let params = extract_service_params(&headers);
390 let req = GetExtendedAgentCardRequest { tenant: None };
391 match state.handler.get_extended_agent_card(¶ms, req).await {
392 Ok(card) => protojson_json_response(&card),
393 Err(e) => rest_error_response(e),
394 }
395}
396
397fn protojson_json_response<T: ProtoJsonPayload>(value: &T) -> axum::response::Response {
398 match protojson_conv::to_value(value) {
399 Ok(payload) => Json(payload).into_response(),
400 Err(e) => rest_error_response(A2AError::internal(format!(
401 "failed to serialize ProtoJSON payload: {e}"
402 ))),
403 }
404}
405
406fn protojson_stream(
407 stream: BoxStream<'static, Result<StreamResponse, A2AError>>,
408) -> BoxStream<'static, Result<Value, A2AError>> {
409 Box::pin(stream.map(|item| {
410 item.and_then(|value| {
411 protojson_conv::to_value(&value).map_err(|e| {
412 A2AError::internal(format!("failed to serialize ProtoJSON stream payload: {e}"))
413 })
414 })
415 }))
416}
417
418fn parse_create_push_config_request(
419 task_id: String,
420 raw_config: Value,
421) -> Result<TaskPushNotificationConfig, String> {
422 protojson_conv::from_value::<TaskPushNotificationConfig>(raw_config)
423 .map_err(|e| e.to_string())
424 .and_then(|mut config| {
425 if !config.task_id.is_empty() && config.task_id != task_id {
426 Err(format!(
427 "taskId in request body ({}) did not match task id in path ({task_id})",
428 config.task_id
429 ))
430 } else {
431 config.task_id = task_id;
432 Ok(config)
433 }
434 })
435}
436
437fn rest_payload_error_response(error: impl std::fmt::Display) -> axum::response::Response {
438 let violation = FieldViolation {
439 field: String::new(),
440 description: error.to_string(),
441 };
442 rest_error_response(
443 A2AError::invalid_params("invalid request body")
444 .with_details(vec![TypedDetail::bad_request(vec![violation])]),
445 )
446}
447
448fn rest_error_response(err: A2AError) -> axum::response::Response {
449 let status =
450 StatusCode::from_u16(err.http_status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
451 let reason = error_reason(err.code);
452 let grpc_status = rest_grpc_status(err.code);
453
454 let mut metadata = HashMap::from([("timestamp".to_string(), Utc::now().to_rfc3339())]);
455 let mut details = Vec::new();
456
457 if let Some(extra) = err.details {
458 for detail in extra {
459 if detail.type_url == errordetails::ERROR_INFO_TYPE {
460 if let Some(Value::Object(meta)) = detail.value.get("metadata") {
461 for (k, v) in meta {
462 if let Some(s) = v.as_str() {
463 metadata.entry(k.clone()).or_insert_with(|| s.to_string());
464 }
465 }
466 }
467 } else {
468 details.push(detail);
469 }
470 }
471 }
472
473 details.push(TypedDetail::error_info(
474 reason,
475 errordetails::PROTOCOL_DOMAIN,
476 Some(metadata),
477 ));
478
479 let body = RestErrorEnvelope {
480 error: RestErrorStatus {
481 code: status.as_u16(),
482 status: grpc_status,
483 message: err.message,
484 details,
485 },
486 };
487
488 (status, Json(body)).into_response()
489}
490
491fn rest_grpc_status(code: i32) -> &'static str {
492 match code {
493 error_code::TASK_NOT_FOUND => "NOT_FOUND",
494 error_code::METHOD_NOT_FOUND => "UNIMPLEMENTED",
495 error_code::TASK_NOT_CANCELABLE
496 | error_code::EXTENDED_CARD_NOT_CONFIGURED
497 | error_code::EXTENSION_SUPPORT_REQUIRED
498 | error_code::PUSH_NOTIFICATION_NOT_SUPPORTED
499 | error_code::UNSUPPORTED_OPERATION
500 | error_code::VERSION_NOT_SUPPORTED => "FAILED_PRECONDITION",
501 error_code::CONTENT_TYPE_NOT_SUPPORTED
502 | error_code::PARSE_ERROR
503 | error_code::INVALID_REQUEST
504 | error_code::INVALID_PARAMS => "INVALID_ARGUMENT",
505 _ => "INTERNAL",
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512 use crate::InMemoryPushConfigStore;
513 use crate::executor::ExecutorContext;
514 use crate::handler::DefaultRequestHandler;
515 use crate::task_store::InMemoryTaskStore;
516 use a2a_pb::protojson_conv;
517 use axum::body::Body;
518 use axum::http::Request;
519 use futures::stream::BoxStream;
520 use http_body_util::BodyExt;
521
522 use crate::test_util::{CapturingHandler, assert_header_captured, install_crypto_provider};
523 use tower::ServiceExt;
524
525 struct EchoExecutor;
526
527 impl crate::AgentExecutor for EchoExecutor {
528 fn execute(
529 &self,
530 ctx: ExecutorContext,
531 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
532 let task = Task {
533 id: ctx.task_id.clone(),
534 context_id: ctx.context_id.clone(),
535 status: TaskStatus {
536 state: TaskState::Completed,
537 message: ctx.message,
538 timestamp: None,
539 },
540 artifacts: None,
541 history: None,
542 metadata: None,
543 };
544 Box::pin(futures::stream::once(async move {
545 Ok(StreamResponse::Task(task))
546 }))
547 }
548
549 fn cancel(
550 &self,
551 ctx: ExecutorContext,
552 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
553 let task = Task {
554 id: ctx.task_id.clone(),
555 context_id: ctx.context_id.clone(),
556 status: TaskStatus {
557 state: TaskState::Canceled,
558 message: None,
559 timestamp: None,
560 },
561 artifacts: None,
562 history: None,
563 metadata: None,
564 };
565 Box::pin(futures::stream::once(async move {
566 Ok(StreamResponse::Task(task))
567 }))
568 }
569 }
570
571 fn make_app() -> axum::Router {
572 let handler = Arc::new(DefaultRequestHandler::new(
573 EchoExecutor,
574 InMemoryTaskStore::new(),
575 ));
576 rest_router(handler)
577 }
578
579 fn make_push_app() -> axum::Router {
580 install_crypto_provider();
581 let handler = Arc::new(
582 DefaultRequestHandler::new(EchoExecutor, InMemoryTaskStore::new())
583 .with_push_config_store(InMemoryPushConfigStore::new()),
584 );
585 rest_router(handler)
586 }
587
588 #[tokio::test]
589 async fn test_send_message() {
590 let app = make_app();
591 let body = serde_json::json!({
592 "message": {
593 "messageId": "m1",
594 "role": "ROLE_USER",
595 "parts": [{"text": "hello"}]
596 }
597 });
598 let req = Request::builder()
599 .uri(REST_SEND_MESSAGE_PATH)
600 .method("POST")
601 .header("content-type", "application/json")
602 .body(Body::from(serde_json::to_string(&body).unwrap()))
603 .unwrap();
604 let resp = app.oneshot(req).await.unwrap();
605 assert_eq!(resp.status(), StatusCode::OK);
606 }
607
608 #[tokio::test]
609 async fn test_send_message_legacy_alias() {
610 let app = make_app();
611 let body = serde_json::json!({
612 "message": {
613 "messageId": "m1",
614 "role": "ROLE_USER",
615 "parts": [{"text": "hello"}]
616 }
617 });
618 let req = Request::builder()
619 .uri(REST_SEND_MESSAGE_LEGACY_PATH)
620 .method("POST")
621 .header("content-type", "application/json")
622 .body(Body::from(serde_json::to_string(&body).unwrap()))
623 .unwrap();
624 let resp = app.oneshot(req).await.unwrap();
625 assert_eq!(resp.status(), StatusCode::OK);
626 }
627
628 #[tokio::test]
629 async fn test_get_task_not_found() {
630 let app = make_app();
631 let req = Request::builder()
632 .uri("/tasks/nonexistent")
633 .method("GET")
634 .body(Body::empty())
635 .unwrap();
636 let resp = app.oneshot(req).await.unwrap();
637 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
638 }
639
640 #[tokio::test]
641 async fn test_list_tasks_empty() {
642 let app = make_app();
643 let req = Request::builder()
644 .uri("/tasks")
645 .method("GET")
646 .body(Body::empty())
647 .unwrap();
648 let resp = app.oneshot(req).await.unwrap();
649 assert_eq!(resp.status(), StatusCode::OK);
650 let body = resp.into_body().collect().await.unwrap().to_bytes();
651 let list_resp =
652 protojson_conv::from_str::<ListTasksResponse>(std::str::from_utf8(&body).unwrap())
653 .unwrap();
654 assert!(list_resp.tasks.is_empty());
655 }
656
657 #[tokio::test]
658 async fn test_cancel_task_not_found() {
659 let app = make_app();
660 let req = Request::builder()
661 .uri("/tasks/nonexistent:cancel")
662 .method("POST")
663 .header("content-type", "application/json")
664 .body(Body::empty())
665 .unwrap();
666 let resp = app.oneshot(req).await.unwrap();
667 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
668 }
669
670 #[tokio::test]
671 async fn test_create_push_config_not_supported() {
672 let app = make_app();
673 let body = serde_json::json!({
674 "url": "http://example.com/callback"
675 });
676 let req = Request::builder()
677 .uri("/tasks/t1/pushNotificationConfigs")
678 .method("POST")
679 .header("content-type", "application/json")
680 .body(Body::from(serde_json::to_string(&body).unwrap()))
681 .unwrap();
682 let resp = app.oneshot(req).await.unwrap();
683 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
684 }
685
686 #[tokio::test]
687 async fn test_create_push_config_accepts_raw_config_body() {
688 let app = make_push_app();
689 let body = serde_json::json!({
690 "id": "cfg1",
691 "url": "http://example.com/callback"
692 });
693 let req = Request::builder()
694 .uri("/tasks/t1/pushNotificationConfigs")
695 .method("POST")
696 .header("content-type", "application/json")
697 .body(Body::from(serde_json::to_string(&body).unwrap()))
698 .unwrap();
699 let resp = app.oneshot(req).await.unwrap();
700 assert_eq!(resp.status(), StatusCode::OK);
701 let body = resp.into_body().collect().await.unwrap().to_bytes();
702 let payload = serde_json::from_slice::<TaskPushNotificationConfig>(&body).unwrap();
703 assert_eq!(payload.task_id, "t1");
704 assert_eq!(payload.id.as_deref(), Some("cfg1"));
705 assert_eq!(payload.url, "http://example.com/callback");
706 }
707
708 #[tokio::test]
709 async fn test_create_push_config_accepts_flat_request_body() {
710 let app = make_push_app();
711 let body = serde_json::json!({
712 "taskId": "t1",
713 "id": "cfg1",
714 "url": "http://example.com/callback"
715 });
716 let req = Request::builder()
717 .uri("/tasks/t1/pushNotificationConfigs")
718 .method("POST")
719 .header("content-type", "application/json")
720 .body(Body::from(serde_json::to_string(&body).unwrap()))
721 .unwrap();
722 let resp = app.oneshot(req).await.unwrap();
723 assert_eq!(resp.status(), StatusCode::OK);
724 let body = resp.into_body().collect().await.unwrap().to_bytes();
725 let payload = serde_json::from_slice::<TaskPushNotificationConfig>(&body).unwrap();
726 assert_eq!(payload.task_id, "t1");
727 assert_eq!(payload.id.as_deref(), Some("cfg1"));
728 assert_eq!(payload.url, "http://example.com/callback");
729 }
730
731 #[tokio::test]
732 async fn test_create_push_config_rejects_mismatched_task_id_in_body() {
733 let app = make_push_app();
734 let body = serde_json::json!({
735 "taskId": "other-task",
736 "url": "http://example.com/callback"
737 });
738 let req = Request::builder()
739 .uri("/tasks/t1/pushNotificationConfigs")
740 .method("POST")
741 .header("content-type", "application/json")
742 .body(Body::from(serde_json::to_string(&body).unwrap()))
743 .unwrap();
744 let resp = app.oneshot(req).await.unwrap();
745 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
746 }
747
748 #[tokio::test]
749 async fn test_get_extended_agent_card() {
750 let app = make_app();
751 let req = Request::builder()
752 .uri(REST_EXTENDED_AGENT_CARD_PATH)
753 .method("GET")
754 .body(Body::empty())
755 .unwrap();
756 let resp = app.oneshot(req).await.unwrap();
757 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
758 }
759
760 #[tokio::test]
761 async fn test_rest_error_response_status_codes() {
762 let resp = rest_error_response(A2AError::task_not_found("t1"));
763 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
764
765 let body = resp.into_body().collect().await.unwrap().to_bytes();
766 let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
767 assert_eq!(payload["error"]["code"], 404);
768 assert_eq!(payload["error"]["status"], "NOT_FOUND");
769 assert_eq!(payload["error"]["details"][0]["reason"], "TASK_NOT_FOUND");
770
771 let resp = rest_error_response(A2AError::internal("boom"));
772 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
773
774 let resp = rest_error_response(A2AError::content_type_not_supported());
775 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
776 }
777
778 #[tokio::test]
779 async fn test_rest_error_response_merges_error_info_metadata() {
780 let existing_info = TypedDetail::error_info(
781 "TASK_NOT_FOUND",
782 errordetails::PROTOCOL_DOMAIN,
783 Some(HashMap::from([("taskId".to_string(), "t99".to_string())])),
784 );
785 let err = A2AError::task_not_found("t99").with_details(vec![existing_info]);
786 let resp = rest_error_response(err);
787 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
788
789 let body = resp.into_body().collect().await.unwrap().to_bytes();
790 let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
791 let details = payload["error"]["details"].as_array().unwrap();
792 assert_eq!(details.len(), 1);
794 assert_eq!(details[0]["@type"], errordetails::ERROR_INFO_TYPE);
795 assert_eq!(details[0]["reason"], "TASK_NOT_FOUND");
796 assert_eq!(details[0]["metadata"]["taskId"], "t99");
798 assert!(details[0]["metadata"]["timestamp"].is_string());
799 }
800
801 #[tokio::test]
802 async fn test_rest_error_response_preserves_non_error_info_details() {
803 let bad_req = TypedDetail::bad_request(vec![FieldViolation {
804 field: "message.parts".into(),
805 description: "required".into(),
806 }]);
807 let err = A2AError::invalid_params("bad request").with_details(vec![bad_req]);
808 let resp = rest_error_response(err);
809 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
810
811 let body = resp.into_body().collect().await.unwrap().to_bytes();
812 let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
813 let details = payload["error"]["details"].as_array().unwrap();
814 assert_eq!(details.len(), 2);
816 assert_eq!(details[0]["@type"], errordetails::BAD_REQUEST_TYPE);
817 assert_eq!(details[0]["fieldViolations"][0]["field"], "message.parts");
818 assert_eq!(details[1]["@type"], errordetails::ERROR_INFO_TYPE);
819 assert_eq!(details[1]["reason"], "INVALID_PARAMS");
820 }
821
822 #[tokio::test]
823 async fn test_rest_payload_error_response() {
824 let resp = rest_payload_error_response("missing field 'parts'");
825 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
826
827 let body = resp.into_body().collect().await.unwrap().to_bytes();
828 let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
829 assert_eq!(
830 payload["error"]["message"].as_str().unwrap(),
831 "invalid request body"
832 );
833 let details = payload["error"]["details"].as_array().unwrap();
834 assert_eq!(details.len(), 2);
836 assert_eq!(details[0]["@type"], errordetails::BAD_REQUEST_TYPE);
837 assert_eq!(
838 details[0]["fieldViolations"][0]["description"],
839 "missing field 'parts'"
840 );
841 assert_eq!(details[1]["@type"], errordetails::ERROR_INFO_TYPE);
842 assert_eq!(details[1]["reason"], "INVALID_PARAMS");
843 }
844
845 #[tokio::test]
846 async fn test_subscribe_to_task_not_found() {
847 let app = make_app();
848 let req = Request::builder()
849 .uri("/tasks/nonexistent:subscribe")
850 .method("GET")
851 .body(Body::empty())
852 .unwrap();
853 let resp = app.oneshot(req).await.unwrap();
854 assert!(resp.status().is_client_error());
855 }
856
857 #[tokio::test]
858 async fn test_stream_message() {
859 let app = make_app();
860 let body = serde_json::json!({
861 "message": {
862 "messageId": "m1",
863 "role": "ROLE_USER",
864 "parts": [{"text": "hello"}]
865 }
866 });
867 let req = Request::builder()
868 .uri(REST_STREAM_MESSAGE_PATH)
869 .method("POST")
870 .header("content-type", "application/json")
871 .header("accept", "text/event-stream")
872 .body(Body::from(serde_json::to_string(&body).unwrap()))
873 .unwrap();
874 let resp = app.oneshot(req).await.unwrap();
875 assert_eq!(resp.status(), StatusCode::OK);
876 }
877
878 #[tokio::test]
879 async fn test_get_push_config_not_found() {
880 let app = make_app();
881 let req = Request::builder()
882 .uri("/tasks/t1/pushNotificationConfigs/cfg1")
883 .method("GET")
884 .body(Body::empty())
885 .unwrap();
886 let resp = app.oneshot(req).await.unwrap();
887 assert!(resp.status().is_client_error());
889 }
890
891 #[tokio::test]
892 async fn test_list_push_configs() {
893 let app = make_push_app();
894 let req = Request::builder()
895 .uri("/tasks/t1/pushNotificationConfigs")
896 .method("GET")
897 .body(Body::empty())
898 .unwrap();
899 let resp = app.oneshot(req).await.unwrap();
900 assert_eq!(resp.status(), StatusCode::OK);
901 let body = resp.into_body().collect().await.unwrap().to_bytes();
902 let payload =
903 serde_json::from_slice::<ListTaskPushNotificationConfigsResponse>(&body).unwrap();
904 assert!(payload.configs.is_empty());
905 }
906
907 #[tokio::test]
908 async fn test_delete_push_config() {
909 let app = make_app();
910 let req = Request::builder()
911 .uri("/tasks/t1/pushNotificationConfigs/cfg1")
912 .method("DELETE")
913 .body(Body::empty())
914 .unwrap();
915 let resp = app.oneshot(req).await.unwrap();
916 assert!(resp.status().is_client_error());
917 }
918
919 #[tokio::test]
920 async fn test_delete_push_config_with_store_returns_ok() {
921 let app = make_push_app();
922 let create_body = serde_json::json!({
923 "id": "cfg1",
924 "url": "http://example.com/callback"
925 });
926 let create_req = Request::builder()
927 .uri("/tasks/t1/pushNotificationConfigs")
928 .method("POST")
929 .header("content-type", "application/json")
930 .body(Body::from(serde_json::to_string(&create_body).unwrap()))
931 .unwrap();
932 let create_resp = app.clone().oneshot(create_req).await.unwrap();
933 assert_eq!(create_resp.status(), StatusCode::OK);
934
935 let delete_req = Request::builder()
936 .uri("/tasks/t1/pushNotificationConfigs/cfg1")
937 .method("DELETE")
938 .body(Body::empty())
939 .unwrap();
940 let delete_resp = app.clone().oneshot(delete_req).await.unwrap();
941 assert_eq!(delete_resp.status(), StatusCode::OK);
942
943 let list_req = Request::builder()
944 .uri("/tasks/t1/pushNotificationConfigs")
945 .method("GET")
946 .body(Body::empty())
947 .unwrap();
948 let list_resp = app.oneshot(list_req).await.unwrap();
949 assert_eq!(list_resp.status(), StatusCode::OK);
950 let body = list_resp.into_body().collect().await.unwrap().to_bytes();
951 let payload =
952 serde_json::from_slice::<ListTaskPushNotificationConfigsResponse>(&body).unwrap();
953 assert!(payload.configs.is_empty());
954 }
955
956 #[tokio::test]
957 async fn test_list_tasks_with_query_params() {
958 let app = make_app();
959 let req = Request::builder()
960 .uri("/tasks?contextId=c1&pageSize=5&pageToken=0&historyLength=3&includeArtifacts=true&statusTimestampAfter=2025-01-01T00:00:00Z")
961 .method("GET")
962 .body(Body::empty())
963 .unwrap();
964 let resp = app.oneshot(req).await.unwrap();
965 assert_eq!(resp.status(), StatusCode::OK);
966 }
967
968 #[tokio::test]
969 async fn test_list_tasks_invalid_status_returns_bad_request() {
970 let app = make_app();
971 let req = Request::builder()
972 .uri("/tasks?status=invalid")
973 .method("GET")
974 .body(Body::empty())
975 .unwrap();
976 let resp = app.oneshot(req).await.unwrap();
977 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
978 let body = resp.into_body().collect().await.unwrap().to_bytes();
979 let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
980 assert_eq!(payload["error"]["message"], "invalid status filter");
981 }
982
983 #[tokio::test]
984 async fn test_send_message_propagates_headers_as_service_params() {
985 let (handler, captured) = CapturingHandler::new();
986 let app = rest_router(handler);
987
988 let body = serde_json::json!({
989 "message": {
990 "messageId": "m1",
991 "role": "ROLE_USER",
992 "parts": [{"text": "hi"}]
993 }
994 });
995 let req = Request::builder()
996 .uri(REST_SEND_MESSAGE_PATH)
997 .method("POST")
998 .header("content-type", "application/json")
999 .header("authorization", "Bearer test-token-abc")
1000 .header("x-tenant-id", "acme")
1001 .body(Body::from(serde_json::to_string(&body).unwrap()))
1002 .unwrap();
1003 let _ = app.oneshot(req).await.unwrap();
1004
1005 assert_header_captured(
1006 &captured,
1007 "send_message",
1008 "authorization",
1009 "Bearer test-token-abc",
1010 );
1011 assert_header_captured(&captured, "send_message", "x-tenant-id", "acme");
1012 }
1013
1014 #[tokio::test]
1015 async fn test_stream_message_propagates_headers_as_service_params() {
1016 let (handler, captured) = CapturingHandler::new();
1017 let app = rest_router(handler);
1018
1019 let body = serde_json::json!({
1020 "message": {
1021 "messageId": "m1",
1022 "role": "ROLE_USER",
1023 "parts": [{"text": "hi"}]
1024 }
1025 });
1026 let req = Request::builder()
1027 .uri(REST_STREAM_MESSAGE_PATH)
1028 .method("POST")
1029 .header("content-type", "application/json")
1030 .header("accept", "text/event-stream")
1031 .header("authorization", "Bearer streaming-token")
1032 .body(Body::from(serde_json::to_string(&body).unwrap()))
1033 .unwrap();
1034 let resp = app.oneshot(req).await.unwrap();
1035 let _ = resp.into_body().collect().await.unwrap().to_bytes();
1037
1038 assert_header_captured(
1039 &captured,
1040 "send_streaming_message",
1041 "authorization",
1042 "Bearer streaming-token",
1043 );
1044 }
1045
1046 #[tokio::test]
1047 async fn test_get_task_propagates_headers_as_service_params() {
1048 let (handler, captured) = CapturingHandler::new();
1049 let app = rest_router(handler);
1050 let req = Request::builder()
1051 .uri("/tasks/some-id")
1052 .method("GET")
1053 .header("authorization", "Bearer get-task-token")
1054 .header("x-tenant-id", "acme")
1055 .body(Body::empty())
1056 .unwrap();
1057 let _ = app.oneshot(req).await.unwrap();
1058 assert_header_captured(
1059 &captured,
1060 "get_task",
1061 "authorization",
1062 "Bearer get-task-token",
1063 );
1064 assert_header_captured(&captured, "get_task", "x-tenant-id", "acme");
1065 }
1066
1067 #[tokio::test]
1068 async fn test_list_tasks_propagates_headers_as_service_params() {
1069 let (handler, captured) = CapturingHandler::new();
1070 let app = rest_router(handler);
1071 let req = Request::builder()
1072 .uri("/tasks")
1073 .method("GET")
1074 .header("authorization", "Bearer list-tasks-token")
1075 .body(Body::empty())
1076 .unwrap();
1077 let _ = app.oneshot(req).await.unwrap();
1078 assert_header_captured(
1079 &captured,
1080 "list_tasks",
1081 "authorization",
1082 "Bearer list-tasks-token",
1083 );
1084 }
1085
1086 #[tokio::test]
1087 async fn test_cancel_task_propagates_headers_as_service_params() {
1088 let (handler, captured) = CapturingHandler::new();
1089 let app = rest_router(handler);
1090 let req = Request::builder()
1091 .uri("/tasks/some-id:cancel")
1092 .method("POST")
1093 .header("authorization", "Bearer cancel-token")
1094 .body(Body::empty())
1095 .unwrap();
1096 let _ = app.oneshot(req).await.unwrap();
1097 assert_header_captured(
1098 &captured,
1099 "cancel_task",
1100 "authorization",
1101 "Bearer cancel-token",
1102 );
1103 }
1104
1105 #[tokio::test]
1106 async fn test_cancel_task_legacy_propagates_headers_as_service_params() {
1107 let (handler, captured) = CapturingHandler::new();
1109 let app = rest_router(handler);
1110 let req = Request::builder()
1111 .uri("/tasks/some-id/cancel")
1112 .method("POST")
1113 .header("authorization", "Bearer cancel-legacy-token")
1114 .body(Body::empty())
1115 .unwrap();
1116 let _ = app.oneshot(req).await.unwrap();
1117 assert_header_captured(
1118 &captured,
1119 "cancel_task",
1120 "authorization",
1121 "Bearer cancel-legacy-token",
1122 );
1123 }
1124
1125 #[tokio::test]
1126 async fn test_create_push_config_propagates_headers_as_service_params() {
1127 let (handler, captured) = CapturingHandler::new();
1128 let app = rest_router(handler);
1129 let body = serde_json::json!({
1130 "id": "cfg1",
1131 "url": "http://example.com/callback"
1132 });
1133 let req = Request::builder()
1134 .uri("/tasks/t1/pushNotificationConfigs")
1135 .method("POST")
1136 .header("content-type", "application/json")
1137 .header("authorization", "Bearer push-create-token")
1138 .body(Body::from(serde_json::to_string(&body).unwrap()))
1139 .unwrap();
1140 let _ = app.oneshot(req).await.unwrap();
1141 assert_header_captured(
1142 &captured,
1143 "create_push_config",
1144 "authorization",
1145 "Bearer push-create-token",
1146 );
1147 }
1148
1149 #[tokio::test]
1150 async fn test_list_push_configs_propagates_headers_as_service_params() {
1151 let (handler, captured) = CapturingHandler::new();
1152 let app = rest_router(handler);
1153 let req = Request::builder()
1154 .uri("/tasks/t1/pushNotificationConfigs")
1155 .method("GET")
1156 .header("authorization", "Bearer push-list-token")
1157 .body(Body::empty())
1158 .unwrap();
1159 let _ = app.oneshot(req).await.unwrap();
1160 assert_header_captured(
1161 &captured,
1162 "list_push_configs",
1163 "authorization",
1164 "Bearer push-list-token",
1165 );
1166 }
1167
1168 #[tokio::test]
1169 async fn test_get_push_config_propagates_headers_as_service_params() {
1170 let (handler, captured) = CapturingHandler::new();
1171 let app = rest_router(handler);
1172 let req = Request::builder()
1173 .uri("/tasks/t1/pushNotificationConfigs/cfg1")
1174 .method("GET")
1175 .header("authorization", "Bearer push-get-token")
1176 .body(Body::empty())
1177 .unwrap();
1178 let _ = app.oneshot(req).await.unwrap();
1179 assert_header_captured(
1180 &captured,
1181 "get_push_config",
1182 "authorization",
1183 "Bearer push-get-token",
1184 );
1185 }
1186
1187 #[tokio::test]
1188 async fn test_delete_push_config_propagates_headers_as_service_params() {
1189 let (handler, captured) = CapturingHandler::new();
1190 let app = rest_router(handler);
1191 let req = Request::builder()
1192 .uri("/tasks/t1/pushNotificationConfigs/cfg1")
1193 .method("DELETE")
1194 .header("authorization", "Bearer push-delete-token")
1195 .body(Body::empty())
1196 .unwrap();
1197 let _ = app.oneshot(req).await.unwrap();
1198 assert_header_captured(
1199 &captured,
1200 "delete_push_config",
1201 "authorization",
1202 "Bearer push-delete-token",
1203 );
1204 }
1205
1206 #[tokio::test]
1207 async fn test_get_extended_agent_card_propagates_headers_as_service_params() {
1208 let (handler, captured) = CapturingHandler::new();
1209 let app = rest_router(handler);
1210 let req = Request::builder()
1211 .uri(REST_EXTENDED_AGENT_CARD_PATH)
1212 .method("GET")
1213 .header("authorization", "Bearer card-token")
1214 .body(Body::empty())
1215 .unwrap();
1216 let _ = app.oneshot(req).await.unwrap();
1217 assert_header_captured(
1218 &captured,
1219 "get_extended_agent_card",
1220 "authorization",
1221 "Bearer card-token",
1222 );
1223 }
1224
1225 #[tokio::test]
1226 async fn test_subscribe_to_task_propagates_headers_as_service_params() {
1227 let (handler, captured) = CapturingHandler::new();
1228 let app = rest_router(handler);
1229 let req = Request::builder()
1230 .uri("/tasks/some-id:subscribe")
1231 .method("GET")
1232 .header("authorization", "Bearer subscribe-token")
1233 .body(Body::empty())
1234 .unwrap();
1235 let resp = app.oneshot(req).await.unwrap();
1236 let _ = resp.into_body().collect().await.unwrap().to_bytes();
1237 assert_header_captured(
1238 &captured,
1239 "subscribe_to_task",
1240 "authorization",
1241 "Bearer subscribe-token",
1242 );
1243 }
1244
1245 #[tokio::test]
1246 async fn test_subscribe_to_task_legacy_propagates_headers_as_service_params() {
1247 let (handler, captured) = CapturingHandler::new();
1249 let app = rest_router(handler);
1250 let req = Request::builder()
1251 .uri("/tasks/some-id/subscribe")
1252 .method("GET")
1253 .header("authorization", "Bearer subscribe-legacy-token")
1254 .body(Body::empty())
1255 .unwrap();
1256 let resp = app.oneshot(req).await.unwrap();
1257 let _ = resp.into_body().collect().await.unwrap().to_bytes();
1258 assert_header_captured(
1259 &captured,
1260 "subscribe_to_task",
1261 "authorization",
1262 "Bearer subscribe-legacy-token",
1263 );
1264 }
1265
1266 #[tokio::test]
1267 async fn test_get_task_with_history_length() {
1268 let app = make_app();
1270 let body = serde_json::json!({
1272 "message": {
1273 "messageId": "m1",
1274 "role": "ROLE_USER",
1275 "parts": [{"text": "hello"}]
1276 }
1277 });
1278 let req = Request::builder()
1279 .uri(REST_SEND_MESSAGE_PATH)
1280 .method("POST")
1281 .header("content-type", "application/json")
1282 .body(Body::from(serde_json::to_string(&body).unwrap()))
1283 .unwrap();
1284 let resp = app.oneshot(req).await.unwrap();
1285 assert_eq!(resp.status(), StatusCode::OK);
1286 let resp_body = resp.into_body().collect().await.unwrap().to_bytes();
1287 let send_resp: serde_json::Value = serde_json::from_slice(&resp_body).unwrap();
1288 let task_id = send_resp["task"]["id"].as_str().unwrap();
1289
1290 let app = make_app();
1291 let req = Request::builder()
1292 .uri(format!("/tasks/{}?historyLength=5", task_id))
1293 .method("GET")
1294 .body(Body::empty())
1295 .unwrap();
1296 let resp = app.oneshot(req).await.unwrap();
1297 assert!(resp.status() == StatusCode::OK || resp.status() == StatusCode::NOT_FOUND);
1299 }
1300
1301 #[tokio::test]
1305 async fn test_request_body_over_the_limit_is_rejected() {
1306 let app = make_app();
1307 let oversized = Body::from("x".repeat(crate::jsonrpc::MAX_REQUEST_BODY_BYTES + 1));
1308 let req = Request::builder()
1309 .uri(REST_SEND_MESSAGE_PATH)
1310 .method("POST")
1311 .header("content-type", "application/json")
1312 .body(oversized)
1313 .unwrap();
1314
1315 let resp = app.oneshot(req).await.unwrap();
1316 assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
1317 }
1318}