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 headers = extract_headers(req.headers());
191
192 let mut resp =
197 Box::pin(self.dispatch_rest(req, method.as_str(), rest_path, &query, tenant, &headers))
198 .await;
199 if let Some(hval) = self
203 .handler
204 .activated_extensions_header_value(headers.get("a2a-extensions").map(String::as_str))
205 && let Ok(v) = hyper::header::HeaderValue::from_str(&hval)
206 {
207 resp.headers_mut()
208 .insert(a2a_protocol_types::A2A_EXTENSIONS_HEADER, v);
209 }
210 if let Some(ref cors) = self.cors {
211 cors.apply_headers(&mut resp);
212 }
213 resp
214 }
215
216 fn call(
222 &self,
223 http: &HttpRoute<'_>,
224 method: &str,
225 route: &str,
226 headers: &HashMap<String, String>,
227 call: impl std::future::Future<Output = hyper::Response<BoxBody<Bytes, Infallible>>>,
228 ) -> impl std::future::Future<Output = hyper::Response<BoxBody<Bytes, Infallible>>> {
229 let prefixed;
230 let route = if http.tenant_in_path {
231 prefixed = format!("/{{tenant}}{route}");
232 prefixed.as_str()
233 } else {
234 route
235 };
236 crate::rpc_span::ServerSpan::open(
237 &self.handler,
238 crate::rpc_span::RpcSystem::HttpJson,
239 method,
240 Some(headers),
241 )
242 .with_http(http.method, route)
243 .run_response(call)
244 }
245
246 #[allow(clippy::too_many_lines, clippy::literal_string_with_formatting_args)]
250 async fn dispatch_rest(
251 &self,
252 req: hyper::Request<Incoming>,
253 method: &str,
254 path: &str,
255 query: &str,
256 tenant: Option<&str>,
257 headers: &HashMap<String, String>,
258 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
259 let http = HttpRoute {
266 method,
267 tenant_in_path: tenant.is_some(),
268 };
269 let query_tenant = if tenant.is_none() {
270 parse_query_param(query, "tenant").filter(|t| !t.is_empty())
271 } else {
272 None
273 };
274 let tenant = tenant.or(query_tenant.as_deref());
275
276 match (method, path) {
279 ("POST", "/message:send") => {
280 return self
281 .call(
282 &http,
283 "SendMessage",
284 "/message:send",
285 headers,
286 self.handle_send(req, false, tenant, headers),
287 )
288 .await;
289 }
290 ("POST", "/message:stream") => {
291 return self
292 .call(
293 &http,
294 "SendStreamingMessage",
295 "/message:stream",
296 headers,
297 self.handle_send(req, true, tenant, headers),
298 )
299 .await;
300 }
301 _ => {}
302 }
303
304 if let Some(rest) = path.strip_prefix("/tasks/")
306 && let Some((id, action)) = rest.split_once(':')
307 && !id.is_empty()
308 {
309 match (method, action) {
310 ("POST", "cancel") => {
311 return self
312 .call(
313 &http,
314 "CancelTask",
315 "/tasks/{id}:cancel",
316 headers,
317 self.handle_cancel_task(req, id, tenant, headers),
318 )
319 .await;
320 }
321 ("POST" | "GET", "subscribe") => {
330 return self
331 .call(
332 &http,
333 "SubscribeToTask",
334 "/tasks/{id}:subscribe",
335 headers,
336 self.handle_resubscribe(req, id, tenant, headers),
337 )
338 .await;
339 }
340 _ => {}
341 }
342 }
343
344 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
345
346 match (method, segments.as_slice()) {
347 ("GET", ["tasks"]) => {
349 self.call(
350 &http,
351 "ListTasks",
352 "/tasks",
353 headers,
354 self.handle_list_tasks(query, tenant, headers),
355 )
356 .await
357 }
358 ("GET", ["tasks", id]) => {
359 self.call(
360 &http,
361 "GetTask",
362 "/tasks/{id}",
363 headers,
364 self.handle_get_task(id, query, tenant, headers),
365 )
366 .await
367 }
368
369 ("POST", ["tasks", id, "cancel"]) => {
371 self.call(
372 &http,
373 "CancelTask",
374 "/tasks/{id}/cancel",
375 headers,
376 self.handle_cancel_task(req, id, tenant, headers),
377 )
378 .await
379 }
380
381 (
383 "POST",
384 [
385 "tasks",
386 task_id,
387 "pushNotificationConfigs" | "pushNotificationConfig",
388 ],
389 ) => {
390 self.call(
391 &http,
392 "CreateTaskPushNotificationConfig",
393 "/tasks/{task_id}/pushNotificationConfigs",
394 headers,
395 self.handle_set_push_config(req, task_id, tenant, headers),
396 )
397 .await
398 }
399 (
400 "GET",
401 [
402 "tasks",
403 task_id,
404 "pushNotificationConfigs" | "pushNotificationConfig",
405 config_id,
406 ],
407 ) => {
408 self.call(
409 &http,
410 "GetTaskPushNotificationConfig",
411 "/tasks/{task_id}/pushNotificationConfigs/{config_id}",
412 headers,
413 self.handle_get_push_config(task_id, config_id, tenant, headers),
414 )
415 .await
416 }
417 (
418 "GET",
419 [
420 "tasks",
421 task_id,
422 "pushNotificationConfigs" | "pushNotificationConfig",
423 ],
424 ) => {
425 self.call(
426 &http,
427 "ListTaskPushNotificationConfigs",
428 "/tasks/{task_id}/pushNotificationConfigs",
429 headers,
430 self.handle_list_push_configs(task_id, tenant, headers),
431 )
432 .await
433 }
434 (
435 "DELETE",
436 [
437 "tasks",
438 task_id,
439 "pushNotificationConfigs" | "pushNotificationConfig",
440 config_id,
441 ],
442 )
443 | (
444 "POST",
445 [
446 "tasks",
447 task_id,
448 "pushNotificationConfigs" | "pushNotificationConfig",
449 config_id,
450 "delete",
451 ],
452 ) => {
453 self.call(
454 &http,
455 "DeleteTaskPushNotificationConfig",
456 "/tasks/{task_id}/pushNotificationConfigs/{config_id}",
457 headers,
458 self.handle_delete_push_config(task_id, config_id, tenant, headers),
459 )
460 .await
461 }
462
463 ("GET", ["extendedAgentCard"]) => {
465 self.call(
466 &http,
467 "GetExtendedAgentCard",
468 "/extendedAgentCard",
469 headers,
470 self.handle_extended_card(headers),
471 )
472 .await
473 }
474
475 _ => not_found_response(),
476 }
477 }
478
479 #[allow(clippy::result_large_err)]
491 async fn read_body_with_path_fields(
492 &self,
493 req: hyper::Request<Incoming>,
494 tenant: Option<&str>,
495 path_fields: &[(&str, &str)],
496 ) -> Result<serde_json::Value, hyper::Response<BoxBody<Bytes, Infallible>>> {
497 let body_bytes = read_body_limited(
498 req.into_body(),
499 self.config.max_request_body_size,
500 self.config.body_read_timeout,
501 )
502 .await
503 .map_err(|msg| error_json_response(413, &msg))?;
504 let mut value: serde_json::Value = if body_bytes.iter().all(u8::is_ascii_whitespace) {
505 serde_json::Value::Object(serde_json::Map::new())
506 } else {
507 serde_json::from_slice(&body_bytes)
508 .map_err(|e| error_json_response(400, &e.to_string()))?
509 };
510 for &(field, path_value) in path_fields {
511 value = inject_field_if_missing(value, field, path_value);
512 }
513 if let Some(tenant) = tenant
518 && let Some(obj) = value.as_object_mut()
519 {
520 obj.insert(
521 "tenant".to_owned(),
522 serde_json::Value::String(tenant.to_owned()),
523 );
524 }
525 Ok(value)
526 }
527
528 async fn handle_send(
529 &self,
530 req: hyper::Request<Incoming>,
531 streaming: bool,
532 tenant: Option<&str>,
533 headers: &HashMap<String, String>,
534 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
535 let body_value = match self.read_body_with_path_fields(req, tenant, &[]).await {
536 Ok(v) => v,
537 Err(resp) => return resp,
538 };
539 let params: a2a_protocol_types::params::MessageSendParams =
540 match serde_json::from_value(body_value) {
541 Ok(p) => p,
542 Err(e) => return error_json_response(400, &e.to_string()),
543 };
544 match self
545 .handler
546 .on_send_message(params, streaming, Some(headers))
547 .await
548 {
549 Ok(SendMessageResult::Response(resp)) => json_ok_response(&resp),
550 Ok(SendMessageResult::Stream(reader)) => build_sse_response(
551 reader,
552 Some(self.config.sse_keep_alive_interval),
553 Some(self.config.sse_channel_capacity),
554 None, ),
556 Err(e) => server_error_to_response(&e),
557 }
558 }
559
560 async fn handle_get_task(
561 &self,
562 id: &str,
563 query: &str,
564 tenant: Option<&str>,
565 headers: &HashMap<String, String>,
566 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
567 let history_length = parse_query_param_u32(query, "historyLength");
568 let params = a2a_protocol_types::params::TaskQueryParams {
569 tenant: tenant.map(str::to_owned),
570 id: id.to_owned(),
571 history_length,
572 };
573 match self.handler.on_get_task(params, Some(headers)).await {
574 Ok(task) => json_ok_response(&task),
575 Err(e) => server_error_to_response(&e),
576 }
577 }
578
579 async fn handle_list_tasks(
580 &self,
581 query: &str,
582 tenant: Option<&str>,
583 headers: &HashMap<String, String>,
584 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
585 let params = parse_list_tasks_query(query, tenant);
586 match self.handler.on_list_tasks(params, Some(headers)).await {
587 Ok(result) => json_ok_response(&result),
588 Err(e) => server_error_to_response(&e),
589 }
590 }
591
592 async fn handle_cancel_task(
593 &self,
594 req: hyper::Request<Incoming>,
595 id: &str,
596 tenant: Option<&str>,
597 headers: &HashMap<String, String>,
598 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
599 let body_value = match self
603 .read_body_with_path_fields(req, tenant, &[("id", id)])
604 .await
605 {
606 Ok(v) => v,
607 Err(resp) => return resp,
608 };
609 let params: a2a_protocol_types::params::CancelTaskParams =
610 match serde_json::from_value(body_value) {
611 Ok(p) => p,
612 Err(e) => return error_json_response(400, &e.to_string()),
613 };
614 match self.handler.on_cancel_task(params, Some(headers)).await {
615 Ok(task) => json_ok_response(&task),
616 Err(e) => server_error_to_response(&e),
617 }
618 }
619
620 async fn handle_resubscribe(
621 &self,
622 req: hyper::Request<Incoming>,
623 id: &str,
624 tenant: Option<&str>,
625 headers: &HashMap<String, String>,
626 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
627 let body_value = match self
631 .read_body_with_path_fields(req, tenant, &[("id", id)])
632 .await
633 {
634 Ok(v) => v,
635 Err(resp) => return resp,
636 };
637 let params: a2a_protocol_types::params::TaskIdParams =
638 match serde_json::from_value(body_value) {
639 Ok(p) => p,
640 Err(e) => return error_json_response(400, &e.to_string()),
641 };
642 match self.handler.on_resubscribe(params, Some(headers)).await {
643 Ok(reader) => build_sse_response(
644 reader,
645 Some(self.config.sse_keep_alive_interval),
646 Some(self.config.sse_channel_capacity),
647 None, ),
649 Err(e) => server_error_to_response(&e),
650 }
651 }
652
653 async fn handle_set_push_config(
654 &self,
655 req: hyper::Request<Incoming>,
656 task_id: &str,
657 tenant: Option<&str>,
658 headers: &HashMap<String, String>,
659 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
660 let body_value = match self
665 .read_body_with_path_fields(req, tenant, &[("taskId", task_id)])
666 .await
667 {
668 Ok(v) => v,
669 Err(resp) => return resp,
670 };
671 let config: a2a_protocol_types::push::TaskPushNotificationConfig =
672 match serde_json::from_value(body_value) {
673 Ok(c) => c,
674 Err(e) => return error_json_response(400, &e.to_string()),
675 };
676 match self.handler.on_set_push_config(config, Some(headers)).await {
677 Ok(result) => json_ok_response(&result),
678 Err(e) => server_error_to_response(&e),
679 }
680 }
681
682 async fn handle_get_push_config(
683 &self,
684 task_id: &str,
685 config_id: &str,
686 tenant: Option<&str>,
687 headers: &HashMap<String, String>,
688 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
689 let params = a2a_protocol_types::params::GetPushConfigParams {
690 tenant: tenant.map(str::to_owned),
691 task_id: task_id.to_owned(),
692 id: config_id.to_owned(),
693 };
694 match self.handler.on_get_push_config(params, Some(headers)).await {
695 Ok(config) => json_ok_response(&config),
696 Err(e) => server_error_to_response(&e),
697 }
698 }
699
700 async fn handle_list_push_configs(
701 &self,
702 task_id: &str,
703 tenant: Option<&str>,
704 headers: &HashMap<String, String>,
705 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
706 match self
707 .handler
708 .on_list_push_configs(task_id, tenant, Some(headers))
709 .await
710 {
711 Ok(configs) => {
712 let resp = a2a_protocol_types::responses::ListPushConfigsResponse {
713 configs,
714 next_page_token: None,
715 };
716 json_ok_response(&resp)
717 }
718 Err(e) => server_error_to_response(&e),
719 }
720 }
721
722 async fn handle_delete_push_config(
723 &self,
724 task_id: &str,
725 config_id: &str,
726 tenant: Option<&str>,
727 headers: &HashMap<String, String>,
728 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
729 let params = a2a_protocol_types::params::DeletePushConfigParams {
730 tenant: tenant.map(str::to_owned),
731 task_id: task_id.to_owned(),
732 id: config_id.to_owned(),
733 };
734 match self
735 .handler
736 .on_delete_push_config(params, Some(headers))
737 .await
738 {
739 Ok(()) => json_ok_response(&serde_json::json!({})),
740 Err(e) => server_error_to_response(&e),
741 }
742 }
743
744 async fn handle_extended_card(
745 &self,
746 headers: &HashMap<String, String>,
747 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
748 match self.handler.on_get_extended_agent_card(Some(headers)).await {
749 Ok(card) => json_ok_response(&card),
750 Err(e) => server_error_to_response(&e),
751 }
752 }
753}
754
755struct HttpRoute<'a> {
757 method: &'a str,
758 tenant_in_path: bool,
760}
761
762impl std::fmt::Debug for RestDispatcher {
763 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
764 f.debug_struct("RestDispatcher").finish()
765 }
766}
767
768impl crate::serve::Dispatcher for RestDispatcher {
771 fn dispatch(
772 &self,
773 req: hyper::Request<Incoming>,
774 ) -> std::pin::Pin<
775 Box<dyn std::future::Future<Output = crate::serve::DispatchResponse> + Send + '_>,
776 > {
777 Box::pin(self.dispatch(req))
778 }
779
780 fn request_handler(&self) -> Option<&Arc<RequestHandler>> {
781 Some(&self.handler)
782 }
783}
784
785#[cfg(test)]
786mod tests {
787 #[test]
790 fn rest_dispatcher_debug_format() {
791 let debug_output = "RestDispatcher";
794 assert_ne!(debug_output, "");
795 }
796
797 #[test]
798 fn dispatch_config_default_query_limit() {
799 let config = super::super::DispatchConfig::default();
800 assert_eq!(config.max_query_string_length, 4096);
801 }
802}