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_u32, strip_tenant_prefix,
30};
31use response::{
32 error_json_response, extract_headers, health_response, inject_field_if_missing,
33 json_ok_response, not_found_response, read_body_limited, server_error_to_response,
34};
35
36pub struct RestDispatcher {
41 handler: Arc<RequestHandler>,
42 card_handler: Option<StaticAgentCardHandler>,
43 cors: Option<CorsConfig>,
44 config: super::DispatchConfig,
45}
46
47impl RestDispatcher {
48 #[must_use]
50 pub fn new(handler: Arc<RequestHandler>) -> Self {
51 Self::with_config(handler, super::DispatchConfig::default())
52 }
53
54 #[must_use]
56 pub fn with_config(handler: Arc<RequestHandler>, config: super::DispatchConfig) -> Self {
57 let card_handler = handler
58 .agent_card
59 .as_ref()
60 .and_then(|card| StaticAgentCardHandler::new(card).ok());
61 Self {
62 handler,
63 card_handler,
64 cors: None,
65 config,
66 }
67 }
68
69 #[must_use]
74 pub fn with_cors(mut self, cors: CorsConfig) -> Self {
75 self.cors = Some(cors);
76 self
77 }
78
79 #[allow(clippy::too_many_lines)]
81 pub async fn dispatch(
82 &self,
83 req: hyper::Request<Incoming>,
84 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
85 let method = req.method().clone();
86 let path = req.uri().path().to_owned();
87 let query = req.uri().query().unwrap_or("").to_owned();
88 trace_info!(http_method = %method, %path, "dispatching REST request");
89
90 if method == "OPTIONS" {
92 if let Some(ref cors) = self.cors {
93 return cors.preflight_response();
94 }
95 return health_response();
96 }
97
98 if query.len() > self.config.max_query_string_length {
100 let mut resp = error_json_response(
101 414,
102 &format!(
103 "query string too long: {} bytes exceeds {} byte limit",
104 query.len(),
105 self.config.max_query_string_length
106 ),
107 );
108 if let Some(ref cors) = self.cors {
109 cors.apply_headers(&mut resp);
110 }
111 return resp;
112 }
113
114 if method == "GET" && (path == "/health" || path == "/ready") {
116 let mut resp = health_response();
117 if let Some(ref cors) = self.cors {
118 cors.apply_headers(&mut resp);
119 }
120 return resp;
121 }
122
123 if method == "POST" || method == "PUT" || method == "PATCH" {
125 if let Some(ct) = req.headers().get("content-type") {
126 let ct_str = ct.to_str().unwrap_or("");
127 if !ct_str.starts_with("application/json")
128 && !ct_str.starts_with(a2a_protocol_types::A2A_CONTENT_TYPE)
129 {
130 return error_json_response(
131 415,
132 &format!("unsupported Content-Type: {ct_str}; expected application/json or application/a2a+json"),
133 );
134 }
135 }
136 }
137
138 if contains_path_traversal(&path) {
140 return error_json_response(400, "invalid path: path traversal not allowed");
141 }
142
143 if method == "GET" && path == "/.well-known/agent-card.json" {
145 return self
146 .card_handler
147 .as_ref()
148 .map_or_else(not_found_response, |h| {
149 h.handle(&req).map(http_body_util::BodyExt::boxed)
150 });
151 }
152
153 let version_value = req
159 .headers()
160 .get(a2a_protocol_types::A2A_VERSION_HEADER)
161 .and_then(|v| v.to_str().ok());
162 if let Err(err) =
163 super::validate_version_header(version_value, self.config.require_version_header)
164 {
165 return server_error_to_response(&crate::error::ServerError::Protocol(err));
166 }
167
168 let (tenant, rest_path) = strip_tenant_prefix(&path);
170
171 let headers = extract_headers(req.headers());
173
174 let mut resp =
179 Box::pin(self.dispatch_rest(req, method.as_str(), rest_path, &query, tenant, &headers))
180 .await;
181 if let Some(hval) = self
185 .handler
186 .activated_extensions_header_value(headers.get("a2a-extensions").map(String::as_str))
187 {
188 if let Ok(v) = hyper::header::HeaderValue::from_str(&hval) {
189 resp.headers_mut()
190 .insert(a2a_protocol_types::A2A_EXTENSIONS_HEADER, v);
191 }
192 }
193 if let Some(ref cors) = self.cors {
194 cors.apply_headers(&mut resp);
195 }
196 resp
197 }
198
199 #[allow(clippy::too_many_lines)]
201 async fn dispatch_rest(
202 &self,
203 req: hyper::Request<Incoming>,
204 method: &str,
205 path: &str,
206 query: &str,
207 tenant: Option<&str>,
208 headers: &HashMap<String, String>,
209 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
210 match (method, path) {
213 ("POST", "/message:send") => {
214 return self.handle_send(req, false, headers).await;
215 }
216 ("POST", "/message:stream") => {
217 return self.handle_send(req, true, headers).await;
218 }
219 _ => {}
220 }
221
222 if let Some(rest) = path.strip_prefix("/tasks/") {
224 if let Some((id, action)) = rest.split_once(':') {
225 if !id.is_empty() {
226 match (method, action) {
227 ("POST", "cancel") => {
228 return self.handle_cancel_task(id, tenant, headers).await;
229 }
230 ("POST" | "GET", "subscribe") => {
239 return self.handle_resubscribe(id, tenant, headers).await;
240 }
241 _ => {}
242 }
243 }
244 }
245 }
246
247 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
248
249 match (method, segments.as_slice()) {
250 ("GET", ["tasks"]) => self.handle_list_tasks(query, tenant, headers).await,
252 ("GET", ["tasks", id]) => self.handle_get_task(id, query, tenant, headers).await,
253
254 ("POST", ["tasks", id, "cancel"]) => self.handle_cancel_task(id, tenant, headers).await,
256
257 ("POST", ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig"]) => {
259 self.handle_set_push_config(req, task_id, headers).await
260 }
261 (
262 "GET",
263 ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig", config_id],
264 ) => {
265 self.handle_get_push_config(task_id, config_id, tenant, headers)
266 .await
267 }
268 ("GET", ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig"]) => {
269 self.handle_list_push_configs(task_id, tenant, headers)
270 .await
271 }
272 (
273 "DELETE",
274 ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig", config_id],
275 )
276 | (
277 "POST",
278 ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig", config_id, "delete"],
279 ) => {
280 self.handle_delete_push_config(task_id, config_id, tenant, headers)
281 .await
282 }
283
284 ("GET", ["extendedAgentCard"]) => self.handle_extended_card(headers).await,
286
287 _ => not_found_response(),
288 }
289 }
290
291 async fn handle_send(
294 &self,
295 req: hyper::Request<Incoming>,
296 streaming: bool,
297 headers: &HashMap<String, String>,
298 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
299 let body_bytes = match read_body_limited(
300 req.into_body(),
301 self.config.max_request_body_size,
302 self.config.body_read_timeout,
303 )
304 .await
305 {
306 Ok(bytes) => bytes,
307 Err(msg) => return error_json_response(413, &msg),
308 };
309 let params: a2a_protocol_types::params::MessageSendParams =
310 match serde_json::from_slice(&body_bytes) {
311 Ok(p) => p,
312 Err(e) => return error_json_response(400, &e.to_string()),
313 };
314 match self
315 .handler
316 .on_send_message(params, streaming, Some(headers))
317 .await
318 {
319 Ok(SendMessageResult::Response(resp)) => json_ok_response(&resp),
320 Ok(SendMessageResult::Stream(reader)) => build_sse_response(
321 reader,
322 Some(self.config.sse_keep_alive_interval),
323 Some(self.config.sse_channel_capacity),
324 None, ),
326 Err(e) => server_error_to_response(&e),
327 }
328 }
329
330 async fn handle_get_task(
331 &self,
332 id: &str,
333 query: &str,
334 tenant: Option<&str>,
335 headers: &HashMap<String, String>,
336 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
337 let history_length = parse_query_param_u32(query, "historyLength");
338 let params = a2a_protocol_types::params::TaskQueryParams {
339 tenant: tenant.map(str::to_owned),
340 id: id.to_owned(),
341 history_length,
342 };
343 match self.handler.on_get_task(params, Some(headers)).await {
344 Ok(task) => json_ok_response(&task),
345 Err(e) => server_error_to_response(&e),
346 }
347 }
348
349 async fn handle_list_tasks(
350 &self,
351 query: &str,
352 tenant: Option<&str>,
353 headers: &HashMap<String, String>,
354 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
355 let params = parse_list_tasks_query(query, tenant);
356 match self.handler.on_list_tasks(params, Some(headers)).await {
357 Ok(result) => json_ok_response(&result),
358 Err(e) => server_error_to_response(&e),
359 }
360 }
361
362 async fn handle_cancel_task(
363 &self,
364 id: &str,
365 tenant: Option<&str>,
366 headers: &HashMap<String, String>,
367 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
368 let params = a2a_protocol_types::params::CancelTaskParams {
369 tenant: tenant.map(str::to_owned),
370 id: id.to_owned(),
371 metadata: None,
372 };
373 match self.handler.on_cancel_task(params, Some(headers)).await {
374 Ok(task) => json_ok_response(&task),
375 Err(e) => server_error_to_response(&e),
376 }
377 }
378
379 async fn handle_resubscribe(
380 &self,
381 id: &str,
382 tenant: Option<&str>,
383 headers: &HashMap<String, String>,
384 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
385 let params = a2a_protocol_types::params::TaskIdParams {
386 tenant: tenant.map(str::to_owned),
387 id: id.to_owned(),
388 };
389 match self.handler.on_resubscribe(params, Some(headers)).await {
390 Ok(reader) => build_sse_response(
391 reader,
392 Some(self.config.sse_keep_alive_interval),
393 Some(self.config.sse_channel_capacity),
394 None, ),
396 Err(e) => server_error_to_response(&e),
397 }
398 }
399
400 async fn handle_set_push_config(
401 &self,
402 req: hyper::Request<Incoming>,
403 task_id: &str,
404 headers: &HashMap<String, String>,
405 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
406 let body_bytes = match read_body_limited(
407 req.into_body(),
408 self.config.max_request_body_size,
409 self.config.body_read_timeout,
410 )
411 .await
412 {
413 Ok(bytes) => bytes,
414 Err(msg) => return error_json_response(413, &msg),
415 };
416 let body_value: serde_json::Value = match serde_json::from_slice(&body_bytes) {
420 Ok(v) => v,
421 Err(e) => return error_json_response(400, &e.to_string()),
422 };
423 let body_value = inject_field_if_missing(body_value, "taskId", task_id);
424 let config: a2a_protocol_types::push::TaskPushNotificationConfig =
425 match serde_json::from_value(body_value) {
426 Ok(c) => c,
427 Err(e) => return error_json_response(400, &e.to_string()),
428 };
429 match self.handler.on_set_push_config(config, Some(headers)).await {
430 Ok(result) => json_ok_response(&result),
431 Err(e) => server_error_to_response(&e),
432 }
433 }
434
435 async fn handle_get_push_config(
436 &self,
437 task_id: &str,
438 config_id: &str,
439 tenant: Option<&str>,
440 headers: &HashMap<String, String>,
441 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
442 let params = a2a_protocol_types::params::GetPushConfigParams {
443 tenant: tenant.map(str::to_owned),
444 task_id: task_id.to_owned(),
445 id: config_id.to_owned(),
446 };
447 match self.handler.on_get_push_config(params, Some(headers)).await {
448 Ok(config) => json_ok_response(&config),
449 Err(e) => server_error_to_response(&e),
450 }
451 }
452
453 async fn handle_list_push_configs(
454 &self,
455 task_id: &str,
456 tenant: Option<&str>,
457 headers: &HashMap<String, String>,
458 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
459 match self
460 .handler
461 .on_list_push_configs(task_id, tenant, Some(headers))
462 .await
463 {
464 Ok(configs) => {
465 let resp = a2a_protocol_types::responses::ListPushConfigsResponse {
466 configs,
467 next_page_token: None,
468 };
469 json_ok_response(&resp)
470 }
471 Err(e) => server_error_to_response(&e),
472 }
473 }
474
475 async fn handle_delete_push_config(
476 &self,
477 task_id: &str,
478 config_id: &str,
479 tenant: Option<&str>,
480 headers: &HashMap<String, String>,
481 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
482 let params = a2a_protocol_types::params::DeletePushConfigParams {
483 tenant: tenant.map(str::to_owned),
484 task_id: task_id.to_owned(),
485 id: config_id.to_owned(),
486 };
487 match self
488 .handler
489 .on_delete_push_config(params, Some(headers))
490 .await
491 {
492 Ok(()) => json_ok_response(&serde_json::json!({})),
493 Err(e) => server_error_to_response(&e),
494 }
495 }
496
497 async fn handle_extended_card(
498 &self,
499 headers: &HashMap<String, String>,
500 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
501 match self.handler.on_get_extended_agent_card(Some(headers)).await {
502 Ok(card) => json_ok_response(&card),
503 Err(e) => server_error_to_response(&e),
504 }
505 }
506}
507
508impl std::fmt::Debug for RestDispatcher {
509 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
510 f.debug_struct("RestDispatcher").finish()
511 }
512}
513
514impl crate::serve::Dispatcher for RestDispatcher {
517 fn dispatch(
518 &self,
519 req: hyper::Request<Incoming>,
520 ) -> std::pin::Pin<
521 Box<dyn std::future::Future<Output = crate::serve::DispatchResponse> + Send + '_>,
522 > {
523 Box::pin(self.dispatch(req))
524 }
525}
526
527#[cfg(test)]
528mod tests {
529 #[test]
532 fn rest_dispatcher_debug_format() {
533 let debug_output = "RestDispatcher";
536 assert!(!debug_output.is_empty());
537 }
538
539 #[test]
540 fn dispatch_config_default_query_limit() {
541 let config = super::super::DispatchConfig::default();
542 assert_eq!(config.max_query_string_length, 4096);
543 }
544}