1mod error_response;
13pub(crate) mod query;
14mod response;
15
16pub(crate) use error_response::{error_json_response, server_error_to_response};
19pub(crate) use response::json_ok_response;
20
21use std::collections::HashMap;
22use std::convert::Infallible;
23use std::sync::Arc;
24
25use bytes::Bytes;
26use http_body_util::combinators::BoxBody;
27use hyper::body::Incoming;
28
29use crate::agent_card::StaticAgentCardHandler;
30use crate::dispatch::cors::CorsConfig;
31use crate::handler::{RequestHandler, SendMessageResult};
32use crate::streaming::build_sse_response;
33
34use error_response::not_found_response;
35use query::{
36 contains_path_traversal, parse_list_tasks_query, parse_query_param, parse_query_param_u32,
37 percent_decode, strip_tenant_prefix,
38};
39use response::{extract_headers, health_response, inject_field_if_missing, read_body_limited};
40
41pub struct RestDispatcher {
46 handler: Arc<RequestHandler>,
47 card_handler: Option<StaticAgentCardHandler>,
48 cors: Option<CorsConfig>,
49 config: super::DispatchConfig,
50}
51
52impl RestDispatcher {
53 #[must_use]
55 pub fn new(handler: Arc<RequestHandler>) -> Self {
56 Self::with_config(handler, super::DispatchConfig::default())
57 }
58
59 #[must_use]
61 pub fn with_config(handler: Arc<RequestHandler>, config: super::DispatchConfig) -> Self {
62 let card_handler = handler
63 .agent_card
64 .as_ref()
65 .and_then(|card| StaticAgentCardHandler::new(card).ok());
66 Self {
67 handler,
68 card_handler,
69 cors: None,
70 config,
71 }
72 }
73
74 #[must_use]
79 pub fn with_cors(mut self, cors: CorsConfig) -> Self {
80 self.cors = Some(cors);
81 self
82 }
83
84 #[allow(clippy::too_many_lines)]
86 pub async fn dispatch(
87 &self,
88 req: hyper::Request<Incoming>,
89 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
90 let method = req.method().clone();
91 let path = req.uri().path().to_owned();
92 let query = req.uri().query().unwrap_or("").to_owned();
93 trace_info!(http_method = %method, %path, "dispatching REST request");
94
95 if method == "OPTIONS" {
97 if let Some(ref cors) = self.cors {
98 return cors.preflight_response();
99 }
100 return health_response();
101 }
102
103 if query.len() > self.config.max_query_string_length {
105 let mut resp = error_json_response(
106 414,
107 &format!(
108 "query string too long: {} bytes exceeds {} byte limit",
109 query.len(),
110 self.config.max_query_string_length
111 ),
112 );
113 if let Some(ref cors) = self.cors {
114 cors.apply_headers(&mut resp);
115 }
116 return resp;
117 }
118
119 if method == "GET" && (path == "/health" || path == "/ready") {
121 let mut resp = health_response();
122 if let Some(ref cors) = self.cors {
123 cors.apply_headers(&mut resp);
124 }
125 return resp;
126 }
127
128 if (method == "POST" || method == "PUT" || method == "PATCH")
130 && let Some(ct) = req.headers().get("content-type")
131 {
132 let ct_str = ct.to_str().unwrap_or("");
133 if !ct_str.starts_with("application/json")
134 && !ct_str.starts_with(a2a_protocol_types::A2A_CONTENT_TYPE)
135 {
136 return error_json_response(
142 a2a_protocol_types::ErrorCode::ContentTypeNotSupported.http_status(),
143 &format!(
144 "unsupported Content-Type: {ct_str}; expected application/json or application/a2a+json"
145 ),
146 );
147 }
148 }
149
150 if contains_path_traversal(&path) {
152 return error_json_response(400, "invalid path: path traversal not allowed");
153 }
154
155 if method == "GET" && path == "/.well-known/agent-card.json" {
157 return self
158 .card_handler
159 .as_ref()
160 .map_or_else(not_found_response, |h| {
161 h.handle(&req).map(http_body_util::BodyExt::boxed)
162 });
163 }
164
165 let version_value = req
171 .headers()
172 .get(a2a_protocol_types::A2A_VERSION_HEADER)
173 .and_then(|v| v.to_str().ok());
174 if let Err(err) =
175 super::validate_version_header(version_value, self.config.require_version_header)
176 {
177 return server_error_to_response(&crate::error::ServerError::Protocol(err));
178 }
179
180 let (tenant, rest_path) = strip_tenant_prefix(&path);
186 let tenant = tenant.map(percent_decode);
187 let tenant = tenant.as_deref();
188
189 let mut headers = extract_headers(req.headers());
191 headers.insert(":path".to_owned(), path.clone());
195
196 let mut resp =
201 Box::pin(self.dispatch_rest(req, method.as_str(), rest_path, &query, tenant, &headers))
202 .await;
203 if let Some(hval) = self
207 .handler
208 .activated_extensions_header_value(headers.get("a2a-extensions").map(String::as_str))
209 && let Ok(v) = hyper::header::HeaderValue::from_str(&hval)
210 {
211 resp.headers_mut()
212 .insert(a2a_protocol_types::A2A_EXTENSIONS_HEADER, v);
213 }
214 if let Some(ref cors) = self.cors {
215 cors.apply_headers(&mut resp);
216 }
217 resp
218 }
219
220 fn call(
226 &self,
227 http: &HttpRoute<'_>,
228 method: &str,
229 route: &str,
230 headers: &HashMap<String, String>,
231 call: impl std::future::Future<Output = hyper::Response<BoxBody<Bytes, Infallible>>>,
232 ) -> impl std::future::Future<Output = hyper::Response<BoxBody<Bytes, Infallible>>> {
233 let prefixed;
234 let route = if http.tenant_in_path {
235 prefixed = format!("/{{tenant}}{route}");
236 prefixed.as_str()
237 } else {
238 route
239 };
240 crate::rpc_span::ServerSpan::open(
241 &self.handler,
242 crate::rpc_span::RpcSystem::HttpJson,
243 method,
244 Some(headers),
245 )
246 .with_http(http.method, route)
247 .run_response(call)
248 }
249
250 #[allow(clippy::too_many_lines, clippy::literal_string_with_formatting_args)]
254 async fn dispatch_rest(
255 &self,
256 req: hyper::Request<Incoming>,
257 method: &str,
258 path: &str,
259 query: &str,
260 tenant: Option<&str>,
261 headers: &HashMap<String, String>,
262 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
263 let http = HttpRoute {
270 method,
271 tenant_in_path: tenant.is_some(),
272 };
273 let query_tenant = if tenant.is_none() {
274 parse_query_param(query, "tenant").filter(|t| !t.is_empty())
275 } else {
276 None
277 };
278 let tenant = tenant.or(query_tenant.as_deref());
279
280 match (method, path) {
283 ("POST", "/message:send") => {
284 return self
285 .call(
286 &http,
287 "SendMessage",
288 "/message:send",
289 headers,
290 self.handle_send(req, false, tenant, headers),
291 )
292 .await;
293 }
294 ("POST", "/message:stream") => {
295 return self
296 .call(
297 &http,
298 "SendStreamingMessage",
299 "/message:stream",
300 headers,
301 self.handle_send(req, true, tenant, headers),
302 )
303 .await;
304 }
305 _ => {}
306 }
307
308 if let Some(rest) = path.strip_prefix("/tasks/")
310 && let Some((id, action)) = rest.split_once(':')
311 && !id.is_empty()
312 {
313 match (method, action) {
314 ("POST", "cancel") => {
315 return self
316 .call(
317 &http,
318 "CancelTask",
319 "/tasks/{id}:cancel",
320 headers,
321 self.handle_cancel_task(req, id, tenant, headers),
322 )
323 .await;
324 }
325 ("POST" | "GET", "subscribe") => {
334 return self
335 .call(
336 &http,
337 "SubscribeToTask",
338 "/tasks/{id}:subscribe",
339 headers,
340 self.handle_resubscribe(req, id, tenant, headers),
341 )
342 .await;
343 }
344 _ => {}
345 }
346 }
347
348 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
349
350 match (method, segments.as_slice()) {
351 ("GET", ["tasks"]) => {
353 self.call(
354 &http,
355 "ListTasks",
356 "/tasks",
357 headers,
358 self.handle_list_tasks(query, tenant, headers),
359 )
360 .await
361 }
362 ("GET", ["tasks", id]) => {
363 self.call(
364 &http,
365 "GetTask",
366 "/tasks/{id}",
367 headers,
368 self.handle_get_task(id, query, tenant, headers),
369 )
370 .await
371 }
372
373 ("POST", ["tasks", id, "cancel"]) => {
375 self.call(
376 &http,
377 "CancelTask",
378 "/tasks/{id}/cancel",
379 headers,
380 self.handle_cancel_task(req, id, tenant, headers),
381 )
382 .await
383 }
384
385 (
387 "POST",
388 [
389 "tasks",
390 task_id,
391 "pushNotificationConfigs" | "pushNotificationConfig",
392 ],
393 ) => {
394 self.call(
395 &http,
396 "CreateTaskPushNotificationConfig",
397 "/tasks/{task_id}/pushNotificationConfigs",
398 headers,
399 self.handle_set_push_config(req, task_id, tenant, headers),
400 )
401 .await
402 }
403 (
404 "GET",
405 [
406 "tasks",
407 task_id,
408 "pushNotificationConfigs" | "pushNotificationConfig",
409 config_id,
410 ],
411 ) => {
412 self.call(
413 &http,
414 "GetTaskPushNotificationConfig",
415 "/tasks/{task_id}/pushNotificationConfigs/{config_id}",
416 headers,
417 self.handle_get_push_config(task_id, config_id, tenant, headers),
418 )
419 .await
420 }
421 (
422 "GET",
423 [
424 "tasks",
425 task_id,
426 "pushNotificationConfigs" | "pushNotificationConfig",
427 ],
428 ) => {
429 self.call(
430 &http,
431 "ListTaskPushNotificationConfigs",
432 "/tasks/{task_id}/pushNotificationConfigs",
433 headers,
434 self.handle_list_push_configs(task_id, tenant, headers),
435 )
436 .await
437 }
438 (
439 "DELETE",
440 [
441 "tasks",
442 task_id,
443 "pushNotificationConfigs" | "pushNotificationConfig",
444 config_id,
445 ],
446 )
447 | (
448 "POST",
449 [
450 "tasks",
451 task_id,
452 "pushNotificationConfigs" | "pushNotificationConfig",
453 config_id,
454 "delete",
455 ],
456 ) => {
457 self.call(
458 &http,
459 "DeleteTaskPushNotificationConfig",
460 "/tasks/{task_id}/pushNotificationConfigs/{config_id}",
461 headers,
462 self.handle_delete_push_config(task_id, config_id, tenant, headers),
463 )
464 .await
465 }
466
467 ("GET", ["extendedAgentCard"]) => {
469 self.call(
470 &http,
471 "GetExtendedAgentCard",
472 "/extendedAgentCard",
473 headers,
474 self.handle_extended_card(headers),
475 )
476 .await
477 }
478
479 _ => not_found_response(),
480 }
481 }
482
483 #[allow(clippy::result_large_err)]
495 async fn read_body_with_path_fields(
496 &self,
497 req: hyper::Request<Incoming>,
498 tenant: Option<&str>,
499 path_fields: &[(&str, &str)],
500 ) -> Result<serde_json::Value, hyper::Response<BoxBody<Bytes, Infallible>>> {
501 let body_bytes = read_body_limited(
502 req.into_body(),
503 self.config.max_request_body_size,
504 self.config.body_read_timeout,
505 )
506 .await
507 .map_err(|msg| error_json_response(413, &msg))?;
508 let mut value: serde_json::Value = if body_bytes.iter().all(u8::is_ascii_whitespace) {
509 serde_json::Value::Object(serde_json::Map::new())
510 } else {
511 serde_json::from_slice(&body_bytes)
512 .map_err(|e| error_json_response(400, &e.to_string()))?
513 };
514 for &(field, path_value) in path_fields {
515 value = inject_field_if_missing(value, field, path_value);
516 }
517 if let Some(tenant) = tenant
522 && let Some(obj) = value.as_object_mut()
523 {
524 obj.insert(
525 "tenant".to_owned(),
526 serde_json::Value::String(tenant.to_owned()),
527 );
528 }
529 Ok(value)
530 }
531
532 async fn handle_send(
533 &self,
534 req: hyper::Request<Incoming>,
535 streaming: bool,
536 tenant: Option<&str>,
537 headers: &HashMap<String, String>,
538 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
539 let body_value = match self.read_body_with_path_fields(req, tenant, &[]).await {
540 Ok(v) => v,
541 Err(resp) => return resp,
542 };
543 let params: a2a_protocol_types::params::MessageSendParams =
544 match serde_json::from_value(body_value) {
545 Ok(p) => p,
546 Err(e) => return error_json_response(400, &e.to_string()),
547 };
548 match self
549 .handler
550 .on_send_message(params, streaming, Some(headers))
551 .await
552 {
553 Ok(SendMessageResult::Response(resp)) => json_ok_response(&resp),
554 Ok(SendMessageResult::Stream(reader)) => build_sse_response(
555 reader,
556 Some(self.config.sse_keep_alive_interval),
557 Some(self.config.sse_channel_capacity),
558 None, ),
560 Err(e) => server_error_to_response(&e),
561 }
562 }
563
564 async fn handle_get_task(
565 &self,
566 id: &str,
567 query: &str,
568 tenant: Option<&str>,
569 headers: &HashMap<String, String>,
570 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
571 let history_length = parse_query_param_u32(query, "historyLength");
572 let params = a2a_protocol_types::params::TaskQueryParams {
573 tenant: tenant.map(str::to_owned),
574 id: id.to_owned(),
575 history_length,
576 };
577 match self.handler.on_get_task(params, Some(headers)).await {
578 Ok(task) => json_ok_response(&task),
579 Err(e) => server_error_to_response(&e),
580 }
581 }
582
583 async fn handle_list_tasks(
584 &self,
585 query: &str,
586 tenant: Option<&str>,
587 headers: &HashMap<String, String>,
588 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
589 let params = parse_list_tasks_query(query, tenant);
590 match self.handler.on_list_tasks(params, Some(headers)).await {
591 Ok(result) => json_ok_response(&result),
592 Err(e) => server_error_to_response(&e),
593 }
594 }
595
596 async fn handle_cancel_task(
597 &self,
598 req: hyper::Request<Incoming>,
599 id: &str,
600 tenant: Option<&str>,
601 headers: &HashMap<String, String>,
602 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
603 let body_value = match self
607 .read_body_with_path_fields(req, tenant, &[("id", id)])
608 .await
609 {
610 Ok(v) => v,
611 Err(resp) => return resp,
612 };
613 let params: a2a_protocol_types::params::CancelTaskParams =
614 match serde_json::from_value(body_value) {
615 Ok(p) => p,
616 Err(e) => return error_json_response(400, &e.to_string()),
617 };
618 match self.handler.on_cancel_task(params, Some(headers)).await {
619 Ok(task) => json_ok_response(&task),
620 Err(e) => server_error_to_response(&e),
621 }
622 }
623
624 async fn handle_resubscribe(
625 &self,
626 req: hyper::Request<Incoming>,
627 id: &str,
628 tenant: Option<&str>,
629 headers: &HashMap<String, String>,
630 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
631 let body_value = match self
635 .read_body_with_path_fields(req, tenant, &[("id", id)])
636 .await
637 {
638 Ok(v) => v,
639 Err(resp) => return resp,
640 };
641 let params: a2a_protocol_types::params::TaskIdParams =
642 match serde_json::from_value(body_value) {
643 Ok(p) => p,
644 Err(e) => return error_json_response(400, &e.to_string()),
645 };
646 match self.handler.on_resubscribe(params, Some(headers)).await {
647 Ok(reader) => build_sse_response(
648 reader,
649 Some(self.config.sse_keep_alive_interval),
650 Some(self.config.sse_channel_capacity),
651 None, ),
653 Err(e) => server_error_to_response(&e),
654 }
655 }
656
657 async fn handle_set_push_config(
658 &self,
659 req: hyper::Request<Incoming>,
660 task_id: &str,
661 tenant: Option<&str>,
662 headers: &HashMap<String, String>,
663 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
664 let body_value = match self
669 .read_body_with_path_fields(req, tenant, &[("taskId", task_id)])
670 .await
671 {
672 Ok(v) => v,
673 Err(resp) => return resp,
674 };
675 let config: a2a_protocol_types::push::TaskPushNotificationConfig =
676 match serde_json::from_value(body_value) {
677 Ok(c) => c,
678 Err(e) => return error_json_response(400, &e.to_string()),
679 };
680 match self.handler.on_set_push_config(config, Some(headers)).await {
681 Ok(result) => json_ok_response(&result),
682 Err(e) => server_error_to_response(&e),
683 }
684 }
685
686 async fn handle_get_push_config(
687 &self,
688 task_id: &str,
689 config_id: &str,
690 tenant: Option<&str>,
691 headers: &HashMap<String, String>,
692 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
693 let params = a2a_protocol_types::params::GetPushConfigParams {
694 tenant: tenant.map(str::to_owned),
695 task_id: task_id.to_owned(),
696 id: config_id.to_owned(),
697 };
698 match self.handler.on_get_push_config(params, Some(headers)).await {
699 Ok(config) => json_ok_response(&config),
700 Err(e) => server_error_to_response(&e),
701 }
702 }
703
704 async fn handle_list_push_configs(
705 &self,
706 task_id: &str,
707 tenant: Option<&str>,
708 headers: &HashMap<String, String>,
709 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
710 match self
711 .handler
712 .on_list_push_configs(task_id, tenant, Some(headers))
713 .await
714 {
715 Ok(configs) => {
716 let resp = a2a_protocol_types::responses::ListPushConfigsResponse {
717 configs,
718 next_page_token: None,
719 };
720 json_ok_response(&resp)
721 }
722 Err(e) => server_error_to_response(&e),
723 }
724 }
725
726 async fn handle_delete_push_config(
727 &self,
728 task_id: &str,
729 config_id: &str,
730 tenant: Option<&str>,
731 headers: &HashMap<String, String>,
732 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
733 let params = a2a_protocol_types::params::DeletePushConfigParams {
734 tenant: tenant.map(str::to_owned),
735 task_id: task_id.to_owned(),
736 id: config_id.to_owned(),
737 };
738 match self
739 .handler
740 .on_delete_push_config(params, Some(headers))
741 .await
742 {
743 Ok(()) => json_ok_response(&serde_json::json!({})),
744 Err(e) => server_error_to_response(&e),
745 }
746 }
747
748 async fn handle_extended_card(
749 &self,
750 headers: &HashMap<String, String>,
751 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
752 match self.handler.on_get_extended_agent_card(Some(headers)).await {
753 Ok(card) => json_ok_response(&card),
754 Err(e) => server_error_to_response(&e),
755 }
756 }
757}
758
759struct HttpRoute<'a> {
761 method: &'a str,
762 tenant_in_path: bool,
764}
765
766impl std::fmt::Debug for RestDispatcher {
767 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
768 f.debug_struct("RestDispatcher").finish()
769 }
770}
771
772impl crate::serve::Dispatcher for RestDispatcher {
775 fn dispatch(
776 &self,
777 req: hyper::Request<Incoming>,
778 ) -> std::pin::Pin<
779 Box<dyn std::future::Future<Output = crate::serve::DispatchResponse> + Send + '_>,
780 > {
781 Box::pin(self.dispatch(req))
782 }
783
784 fn request_handler(&self) -> Option<&Arc<RequestHandler>> {
785 Some(&self.handler)
786 }
787}
788
789#[cfg(test)]
790mod tests {
791 #[test]
794 fn rest_dispatcher_debug_format() {
795 let debug_output = "RestDispatcher";
798 assert_ne!(debug_output, "");
799 }
800
801 #[test]
802 fn dispatch_config_default_query_limit() {
803 let config = super::super::DispatchConfig::default();
804 assert_eq!(config.max_query_string_length, 4096);
805 }
806}