1mod query;
13mod response;
14
15use std::collections::HashMap;
16use std::convert::Infallible;
17use std::sync::Arc;
18
19use bytes::Bytes;
20use http_body_util::combinators::BoxBody;
21use hyper::body::Incoming;
22
23use crate::agent_card::StaticAgentCardHandler;
24use crate::dispatch::cors::CorsConfig;
25use crate::handler::{RequestHandler, SendMessageResult};
26use crate::streaming::build_sse_response;
27
28use query::{
29 contains_path_traversal, parse_list_tasks_query, parse_query_param, parse_query_param_u32,
30 percent_decode, strip_tenant_prefix,
31};
32use response::{
33 error_json_response, extract_headers, health_response, inject_field_if_missing,
34 json_ok_response, not_found_response, read_body_limited, server_error_to_response,
35};
36
37pub struct RestDispatcher {
42 handler: Arc<RequestHandler>,
43 card_handler: Option<StaticAgentCardHandler>,
44 cors: Option<CorsConfig>,
45 config: super::DispatchConfig,
46}
47
48impl RestDispatcher {
49 #[must_use]
51 pub fn new(handler: Arc<RequestHandler>) -> Self {
52 Self::with_config(handler, super::DispatchConfig::default())
53 }
54
55 #[must_use]
57 pub fn with_config(handler: Arc<RequestHandler>, config: super::DispatchConfig) -> Self {
58 let card_handler = handler
59 .agent_card
60 .as_ref()
61 .and_then(|card| StaticAgentCardHandler::new(card).ok());
62 Self {
63 handler,
64 card_handler,
65 cors: None,
66 config,
67 }
68 }
69
70 #[must_use]
75 pub fn with_cors(mut self, cors: CorsConfig) -> Self {
76 self.cors = Some(cors);
77 self
78 }
79
80 #[allow(clippy::too_many_lines)]
82 pub async fn dispatch(
83 &self,
84 req: hyper::Request<Incoming>,
85 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
86 let method = req.method().clone();
87 let path = req.uri().path().to_owned();
88 let query = req.uri().query().unwrap_or("").to_owned();
89 trace_info!(http_method = %method, %path, "dispatching REST request");
90
91 if method == "OPTIONS" {
93 if let Some(ref cors) = self.cors {
94 return cors.preflight_response();
95 }
96 return health_response();
97 }
98
99 if query.len() > self.config.max_query_string_length {
101 let mut resp = error_json_response(
102 414,
103 &format!(
104 "query string too long: {} bytes exceeds {} byte limit",
105 query.len(),
106 self.config.max_query_string_length
107 ),
108 );
109 if let Some(ref cors) = self.cors {
110 cors.apply_headers(&mut resp);
111 }
112 return resp;
113 }
114
115 if method == "GET" && (path == "/health" || path == "/ready") {
117 let mut resp = health_response();
118 if let Some(ref cors) = self.cors {
119 cors.apply_headers(&mut resp);
120 }
121 return resp;
122 }
123
124 if (method == "POST" || method == "PUT" || method == "PATCH")
126 && let Some(ct) = req.headers().get("content-type")
127 {
128 let ct_str = ct.to_str().unwrap_or("");
129 if !ct_str.starts_with("application/json")
130 && !ct_str.starts_with(a2a_protocol_types::A2A_CONTENT_TYPE)
131 {
132 return error_json_response(
138 a2a_protocol_types::ErrorCode::ContentTypeNotSupported.http_status(),
139 &format!(
140 "unsupported Content-Type: {ct_str}; expected application/json or application/a2a+json"
141 ),
142 );
143 }
144 }
145
146 if contains_path_traversal(&path) {
148 return error_json_response(400, "invalid path: path traversal not allowed");
149 }
150
151 if method == "GET" && path == "/.well-known/agent-card.json" {
153 return self
154 .card_handler
155 .as_ref()
156 .map_or_else(not_found_response, |h| {
157 h.handle(&req).map(http_body_util::BodyExt::boxed)
158 });
159 }
160
161 let version_value = req
167 .headers()
168 .get(a2a_protocol_types::A2A_VERSION_HEADER)
169 .and_then(|v| v.to_str().ok());
170 if let Err(err) =
171 super::validate_version_header(version_value, self.config.require_version_header)
172 {
173 return server_error_to_response(&crate::error::ServerError::Protocol(err));
174 }
175
176 let (tenant, rest_path) = strip_tenant_prefix(&path);
182 let tenant = tenant.map(percent_decode);
183 let tenant = tenant.as_deref();
184
185 let headers = extract_headers(req.headers());
187
188 let mut resp =
193 Box::pin(self.dispatch_rest(req, method.as_str(), rest_path, &query, tenant, &headers))
194 .await;
195 if let Some(hval) = self
199 .handler
200 .activated_extensions_header_value(headers.get("a2a-extensions").map(String::as_str))
201 && let Ok(v) = hyper::header::HeaderValue::from_str(&hval)
202 {
203 resp.headers_mut()
204 .insert(a2a_protocol_types::A2A_EXTENSIONS_HEADER, v);
205 }
206 if let Some(ref cors) = self.cors {
207 cors.apply_headers(&mut resp);
208 }
209 resp
210 }
211
212 #[allow(clippy::too_many_lines)]
214 async fn dispatch_rest(
215 &self,
216 req: hyper::Request<Incoming>,
217 method: &str,
218 path: &str,
219 query: &str,
220 tenant: Option<&str>,
221 headers: &HashMap<String, String>,
222 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
223 let query_tenant = if tenant.is_none() {
230 parse_query_param(query, "tenant").filter(|t| !t.is_empty())
231 } else {
232 None
233 };
234 let tenant = tenant.or(query_tenant.as_deref());
235
236 match (method, path) {
239 ("POST", "/message:send") => {
240 return self.handle_send(req, false, tenant, headers).await;
241 }
242 ("POST", "/message:stream") => {
243 return self.handle_send(req, true, tenant, headers).await;
244 }
245 _ => {}
246 }
247
248 if let Some(rest) = path.strip_prefix("/tasks/")
250 && let Some((id, action)) = rest.split_once(':')
251 && !id.is_empty()
252 {
253 match (method, action) {
254 ("POST", "cancel") => {
255 return self.handle_cancel_task(req, id, tenant, headers).await;
256 }
257 ("POST" | "GET", "subscribe") => {
266 return self.handle_resubscribe(req, id, tenant, headers).await;
267 }
268 _ => {}
269 }
270 }
271
272 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
273
274 match (method, segments.as_slice()) {
275 ("GET", ["tasks"]) => self.handle_list_tasks(query, tenant, headers).await,
277 ("GET", ["tasks", id]) => self.handle_get_task(id, query, tenant, headers).await,
278
279 ("POST", ["tasks", id, "cancel"]) => {
281 self.handle_cancel_task(req, id, tenant, headers).await
282 }
283
284 (
286 "POST",
287 [
288 "tasks",
289 task_id,
290 "pushNotificationConfigs" | "pushNotificationConfig",
291 ],
292 ) => {
293 self.handle_set_push_config(req, task_id, tenant, headers)
294 .await
295 }
296 (
297 "GET",
298 [
299 "tasks",
300 task_id,
301 "pushNotificationConfigs" | "pushNotificationConfig",
302 config_id,
303 ],
304 ) => {
305 self.handle_get_push_config(task_id, config_id, tenant, headers)
306 .await
307 }
308 (
309 "GET",
310 [
311 "tasks",
312 task_id,
313 "pushNotificationConfigs" | "pushNotificationConfig",
314 ],
315 ) => {
316 self.handle_list_push_configs(task_id, tenant, headers)
317 .await
318 }
319 (
320 "DELETE",
321 [
322 "tasks",
323 task_id,
324 "pushNotificationConfigs" | "pushNotificationConfig",
325 config_id,
326 ],
327 )
328 | (
329 "POST",
330 [
331 "tasks",
332 task_id,
333 "pushNotificationConfigs" | "pushNotificationConfig",
334 config_id,
335 "delete",
336 ],
337 ) => {
338 self.handle_delete_push_config(task_id, config_id, tenant, headers)
339 .await
340 }
341
342 ("GET", ["extendedAgentCard"]) => self.handle_extended_card(headers).await,
344
345 _ => not_found_response(),
346 }
347 }
348
349 #[allow(clippy::result_large_err)]
361 async fn read_body_with_path_fields(
362 &self,
363 req: hyper::Request<Incoming>,
364 tenant: Option<&str>,
365 path_fields: &[(&str, &str)],
366 ) -> Result<serde_json::Value, hyper::Response<BoxBody<Bytes, Infallible>>> {
367 let body_bytes = read_body_limited(
368 req.into_body(),
369 self.config.max_request_body_size,
370 self.config.body_read_timeout,
371 )
372 .await
373 .map_err(|msg| error_json_response(413, &msg))?;
374 let mut value: serde_json::Value = if body_bytes.iter().all(u8::is_ascii_whitespace) {
375 serde_json::Value::Object(serde_json::Map::new())
376 } else {
377 serde_json::from_slice(&body_bytes)
378 .map_err(|e| error_json_response(400, &e.to_string()))?
379 };
380 for &(field, path_value) in path_fields {
381 value = inject_field_if_missing(value, field, path_value);
382 }
383 if let Some(tenant) = tenant
388 && let Some(obj) = value.as_object_mut()
389 {
390 obj.insert(
391 "tenant".to_owned(),
392 serde_json::Value::String(tenant.to_owned()),
393 );
394 }
395 Ok(value)
396 }
397
398 async fn handle_send(
399 &self,
400 req: hyper::Request<Incoming>,
401 streaming: bool,
402 tenant: Option<&str>,
403 headers: &HashMap<String, String>,
404 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
405 let body_value = match self.read_body_with_path_fields(req, tenant, &[]).await {
406 Ok(v) => v,
407 Err(resp) => return resp,
408 };
409 let params: a2a_protocol_types::params::MessageSendParams =
410 match serde_json::from_value(body_value) {
411 Ok(p) => p,
412 Err(e) => return error_json_response(400, &e.to_string()),
413 };
414 match self
415 .handler
416 .on_send_message(params, streaming, Some(headers))
417 .await
418 {
419 Ok(SendMessageResult::Response(resp)) => json_ok_response(&resp),
420 Ok(SendMessageResult::Stream(reader)) => build_sse_response(
421 reader,
422 Some(self.config.sse_keep_alive_interval),
423 Some(self.config.sse_channel_capacity),
424 None, ),
426 Err(e) => server_error_to_response(&e),
427 }
428 }
429
430 async fn handle_get_task(
431 &self,
432 id: &str,
433 query: &str,
434 tenant: Option<&str>,
435 headers: &HashMap<String, String>,
436 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
437 let history_length = parse_query_param_u32(query, "historyLength");
438 let params = a2a_protocol_types::params::TaskQueryParams {
439 tenant: tenant.map(str::to_owned),
440 id: id.to_owned(),
441 history_length,
442 };
443 match self.handler.on_get_task(params, Some(headers)).await {
444 Ok(task) => json_ok_response(&task),
445 Err(e) => server_error_to_response(&e),
446 }
447 }
448
449 async fn handle_list_tasks(
450 &self,
451 query: &str,
452 tenant: Option<&str>,
453 headers: &HashMap<String, String>,
454 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
455 let params = parse_list_tasks_query(query, tenant);
456 match self.handler.on_list_tasks(params, Some(headers)).await {
457 Ok(result) => json_ok_response(&result),
458 Err(e) => server_error_to_response(&e),
459 }
460 }
461
462 async fn handle_cancel_task(
463 &self,
464 req: hyper::Request<Incoming>,
465 id: &str,
466 tenant: Option<&str>,
467 headers: &HashMap<String, String>,
468 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
469 let body_value = match self
473 .read_body_with_path_fields(req, tenant, &[("id", id)])
474 .await
475 {
476 Ok(v) => v,
477 Err(resp) => return resp,
478 };
479 let params: a2a_protocol_types::params::CancelTaskParams =
480 match serde_json::from_value(body_value) {
481 Ok(p) => p,
482 Err(e) => return error_json_response(400, &e.to_string()),
483 };
484 match self.handler.on_cancel_task(params, Some(headers)).await {
485 Ok(task) => json_ok_response(&task),
486 Err(e) => server_error_to_response(&e),
487 }
488 }
489
490 async fn handle_resubscribe(
491 &self,
492 req: hyper::Request<Incoming>,
493 id: &str,
494 tenant: Option<&str>,
495 headers: &HashMap<String, String>,
496 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
497 let body_value = match self
501 .read_body_with_path_fields(req, tenant, &[("id", id)])
502 .await
503 {
504 Ok(v) => v,
505 Err(resp) => return resp,
506 };
507 let params: a2a_protocol_types::params::TaskIdParams =
508 match serde_json::from_value(body_value) {
509 Ok(p) => p,
510 Err(e) => return error_json_response(400, &e.to_string()),
511 };
512 match self.handler.on_resubscribe(params, Some(headers)).await {
513 Ok(reader) => build_sse_response(
514 reader,
515 Some(self.config.sse_keep_alive_interval),
516 Some(self.config.sse_channel_capacity),
517 None, ),
519 Err(e) => server_error_to_response(&e),
520 }
521 }
522
523 async fn handle_set_push_config(
524 &self,
525 req: hyper::Request<Incoming>,
526 task_id: &str,
527 tenant: Option<&str>,
528 headers: &HashMap<String, String>,
529 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
530 let body_value = match self
535 .read_body_with_path_fields(req, tenant, &[("taskId", task_id)])
536 .await
537 {
538 Ok(v) => v,
539 Err(resp) => return resp,
540 };
541 let config: a2a_protocol_types::push::TaskPushNotificationConfig =
542 match serde_json::from_value(body_value) {
543 Ok(c) => c,
544 Err(e) => return error_json_response(400, &e.to_string()),
545 };
546 match self.handler.on_set_push_config(config, Some(headers)).await {
547 Ok(result) => json_ok_response(&result),
548 Err(e) => server_error_to_response(&e),
549 }
550 }
551
552 async fn handle_get_push_config(
553 &self,
554 task_id: &str,
555 config_id: &str,
556 tenant: Option<&str>,
557 headers: &HashMap<String, String>,
558 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
559 let params = a2a_protocol_types::params::GetPushConfigParams {
560 tenant: tenant.map(str::to_owned),
561 task_id: task_id.to_owned(),
562 id: config_id.to_owned(),
563 };
564 match self.handler.on_get_push_config(params, Some(headers)).await {
565 Ok(config) => json_ok_response(&config),
566 Err(e) => server_error_to_response(&e),
567 }
568 }
569
570 async fn handle_list_push_configs(
571 &self,
572 task_id: &str,
573 tenant: Option<&str>,
574 headers: &HashMap<String, String>,
575 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
576 match self
577 .handler
578 .on_list_push_configs(task_id, tenant, Some(headers))
579 .await
580 {
581 Ok(configs) => {
582 let resp = a2a_protocol_types::responses::ListPushConfigsResponse {
583 configs,
584 next_page_token: None,
585 };
586 json_ok_response(&resp)
587 }
588 Err(e) => server_error_to_response(&e),
589 }
590 }
591
592 async fn handle_delete_push_config(
593 &self,
594 task_id: &str,
595 config_id: &str,
596 tenant: Option<&str>,
597 headers: &HashMap<String, String>,
598 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
599 let params = a2a_protocol_types::params::DeletePushConfigParams {
600 tenant: tenant.map(str::to_owned),
601 task_id: task_id.to_owned(),
602 id: config_id.to_owned(),
603 };
604 match self
605 .handler
606 .on_delete_push_config(params, Some(headers))
607 .await
608 {
609 Ok(()) => json_ok_response(&serde_json::json!({})),
610 Err(e) => server_error_to_response(&e),
611 }
612 }
613
614 async fn handle_extended_card(
615 &self,
616 headers: &HashMap<String, String>,
617 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
618 match self.handler.on_get_extended_agent_card(Some(headers)).await {
619 Ok(card) => json_ok_response(&card),
620 Err(e) => server_error_to_response(&e),
621 }
622 }
623}
624
625impl std::fmt::Debug for RestDispatcher {
626 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
627 f.debug_struct("RestDispatcher").finish()
628 }
629}
630
631impl crate::serve::Dispatcher for RestDispatcher {
634 fn dispatch(
635 &self,
636 req: hyper::Request<Incoming>,
637 ) -> std::pin::Pin<
638 Box<dyn std::future::Future<Output = crate::serve::DispatchResponse> + Send + '_>,
639 > {
640 Box::pin(self.dispatch(req))
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 #[test]
649 fn rest_dispatcher_debug_format() {
650 let debug_output = "RestDispatcher";
653 assert_ne!(debug_output, "");
654 }
655
656 #[test]
657 fn dispatch_config_default_query_limit() {
658 let config = super::super::DispatchConfig::default();
659 assert_eq!(config.max_query_string_length, 4096);
660 }
661}