Skip to main content

a2a_server/
rest.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3use 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
46/// Shared state for REST handlers.
47pub 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
59/// Create an axum router for the REST/HTTP+JSON protocol binding.
60pub 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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        // Only one ErrorInfo should be present (merged)
793        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        // Merged metadata should contain both taskId and timestamp
797        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        // BadRequest detail + auto-generated ErrorInfo
815        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        // BadRequest detail + ErrorInfo
835        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        // Push not supported → bad request
888        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        // Drain the SSE body to ensure the handler has been fully driven.
1036        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        // /tasks/{id}/cancel routes through handle_cancel_task_legacy.
1108        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        // /tasks/{id}/subscribe routes through handle_subscribe_to_task_legacy.
1248        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        // First create a task, then get it with historyLength
1269        let app = make_app();
1270        // Create task via send_message
1271        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        // Each make_app() creates a new store, so this will be not found
1298        assert!(resp.status() == StatusCode::OK || resp.status() == StatusCode::NOT_FOUND);
1299    }
1300
1301    /// The REST binding is bounded by the same limit as JSON-RPC: an
1302    /// oversized body is rejected at the framework boundary, before any
1303    /// handler runs.
1304    #[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}