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(
136 a2a_protocol_types::ErrorCode::ContentTypeNotSupported.http_status(),
137 &format!("unsupported Content-Type: {ct_str}; expected application/json or application/a2a+json"),
138 );
139 }
140 }
141 }
142
143 if contains_path_traversal(&path) {
145 return error_json_response(400, "invalid path: path traversal not allowed");
146 }
147
148 if method == "GET" && path == "/.well-known/agent-card.json" {
150 return self
151 .card_handler
152 .as_ref()
153 .map_or_else(not_found_response, |h| {
154 h.handle(&req).map(http_body_util::BodyExt::boxed)
155 });
156 }
157
158 let version_value = req
164 .headers()
165 .get(a2a_protocol_types::A2A_VERSION_HEADER)
166 .and_then(|v| v.to_str().ok());
167 if let Err(err) =
168 super::validate_version_header(version_value, self.config.require_version_header)
169 {
170 return server_error_to_response(&crate::error::ServerError::Protocol(err));
171 }
172
173 let (tenant, rest_path) = strip_tenant_prefix(&path);
175
176 let headers = extract_headers(req.headers());
178
179 let mut resp =
184 Box::pin(self.dispatch_rest(req, method.as_str(), rest_path, &query, tenant, &headers))
185 .await;
186 if let Some(hval) = self
190 .handler
191 .activated_extensions_header_value(headers.get("a2a-extensions").map(String::as_str))
192 {
193 if let Ok(v) = hyper::header::HeaderValue::from_str(&hval) {
194 resp.headers_mut()
195 .insert(a2a_protocol_types::A2A_EXTENSIONS_HEADER, v);
196 }
197 }
198 if let Some(ref cors) = self.cors {
199 cors.apply_headers(&mut resp);
200 }
201 resp
202 }
203
204 #[allow(clippy::too_many_lines)]
206 async fn dispatch_rest(
207 &self,
208 req: hyper::Request<Incoming>,
209 method: &str,
210 path: &str,
211 query: &str,
212 tenant: Option<&str>,
213 headers: &HashMap<String, String>,
214 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
215 match (method, path) {
218 ("POST", "/message:send") => {
219 return self.handle_send(req, false, headers).await;
220 }
221 ("POST", "/message:stream") => {
222 return self.handle_send(req, true, headers).await;
223 }
224 _ => {}
225 }
226
227 if let Some(rest) = path.strip_prefix("/tasks/") {
229 if let Some((id, action)) = rest.split_once(':') {
230 if !id.is_empty() {
231 match (method, action) {
232 ("POST", "cancel") => {
233 return self.handle_cancel_task(id, tenant, headers).await;
234 }
235 ("POST" | "GET", "subscribe") => {
244 return self.handle_resubscribe(id, tenant, headers).await;
245 }
246 _ => {}
247 }
248 }
249 }
250 }
251
252 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
253
254 match (method, segments.as_slice()) {
255 ("GET", ["tasks"]) => self.handle_list_tasks(query, tenant, headers).await,
257 ("GET", ["tasks", id]) => self.handle_get_task(id, query, tenant, headers).await,
258
259 ("POST", ["tasks", id, "cancel"]) => self.handle_cancel_task(id, tenant, headers).await,
261
262 ("POST", ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig"]) => {
264 self.handle_set_push_config(req, task_id, headers).await
265 }
266 (
267 "GET",
268 ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig", config_id],
269 ) => {
270 self.handle_get_push_config(task_id, config_id, tenant, headers)
271 .await
272 }
273 ("GET", ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig"]) => {
274 self.handle_list_push_configs(task_id, tenant, headers)
275 .await
276 }
277 (
278 "DELETE",
279 ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig", config_id],
280 )
281 | (
282 "POST",
283 ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig", config_id, "delete"],
284 ) => {
285 self.handle_delete_push_config(task_id, config_id, tenant, headers)
286 .await
287 }
288
289 ("GET", ["extendedAgentCard"]) => self.handle_extended_card(headers).await,
291
292 _ => not_found_response(),
293 }
294 }
295
296 async fn handle_send(
299 &self,
300 req: hyper::Request<Incoming>,
301 streaming: bool,
302 headers: &HashMap<String, String>,
303 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
304 let body_bytes = match read_body_limited(
305 req.into_body(),
306 self.config.max_request_body_size,
307 self.config.body_read_timeout,
308 )
309 .await
310 {
311 Ok(bytes) => bytes,
312 Err(msg) => return error_json_response(413, &msg),
313 };
314 let params: a2a_protocol_types::params::MessageSendParams =
315 match serde_json::from_slice(&body_bytes) {
316 Ok(p) => p,
317 Err(e) => return error_json_response(400, &e.to_string()),
318 };
319 match self
320 .handler
321 .on_send_message(params, streaming, Some(headers))
322 .await
323 {
324 Ok(SendMessageResult::Response(resp)) => json_ok_response(&resp),
325 Ok(SendMessageResult::Stream(reader)) => build_sse_response(
326 reader,
327 Some(self.config.sse_keep_alive_interval),
328 Some(self.config.sse_channel_capacity),
329 None, ),
331 Err(e) => server_error_to_response(&e),
332 }
333 }
334
335 async fn handle_get_task(
336 &self,
337 id: &str,
338 query: &str,
339 tenant: Option<&str>,
340 headers: &HashMap<String, String>,
341 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
342 let history_length = parse_query_param_u32(query, "historyLength");
343 let params = a2a_protocol_types::params::TaskQueryParams {
344 tenant: tenant.map(str::to_owned),
345 id: id.to_owned(),
346 history_length,
347 };
348 match self.handler.on_get_task(params, Some(headers)).await {
349 Ok(task) => json_ok_response(&task),
350 Err(e) => server_error_to_response(&e),
351 }
352 }
353
354 async fn handle_list_tasks(
355 &self,
356 query: &str,
357 tenant: Option<&str>,
358 headers: &HashMap<String, String>,
359 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
360 let params = parse_list_tasks_query(query, tenant);
361 match self.handler.on_list_tasks(params, Some(headers)).await {
362 Ok(result) => json_ok_response(&result),
363 Err(e) => server_error_to_response(&e),
364 }
365 }
366
367 async fn handle_cancel_task(
368 &self,
369 id: &str,
370 tenant: Option<&str>,
371 headers: &HashMap<String, String>,
372 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
373 let params = a2a_protocol_types::params::CancelTaskParams {
374 tenant: tenant.map(str::to_owned),
375 id: id.to_owned(),
376 metadata: None,
377 };
378 match self.handler.on_cancel_task(params, Some(headers)).await {
379 Ok(task) => json_ok_response(&task),
380 Err(e) => server_error_to_response(&e),
381 }
382 }
383
384 async fn handle_resubscribe(
385 &self,
386 id: &str,
387 tenant: Option<&str>,
388 headers: &HashMap<String, String>,
389 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
390 let params = a2a_protocol_types::params::TaskIdParams {
391 tenant: tenant.map(str::to_owned),
392 id: id.to_owned(),
393 };
394 match self.handler.on_resubscribe(params, Some(headers)).await {
395 Ok(reader) => build_sse_response(
396 reader,
397 Some(self.config.sse_keep_alive_interval),
398 Some(self.config.sse_channel_capacity),
399 None, ),
401 Err(e) => server_error_to_response(&e),
402 }
403 }
404
405 async fn handle_set_push_config(
406 &self,
407 req: hyper::Request<Incoming>,
408 task_id: &str,
409 headers: &HashMap<String, String>,
410 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
411 let body_bytes = match read_body_limited(
412 req.into_body(),
413 self.config.max_request_body_size,
414 self.config.body_read_timeout,
415 )
416 .await
417 {
418 Ok(bytes) => bytes,
419 Err(msg) => return error_json_response(413, &msg),
420 };
421 let body_value: serde_json::Value = match serde_json::from_slice(&body_bytes) {
425 Ok(v) => v,
426 Err(e) => return error_json_response(400, &e.to_string()),
427 };
428 let body_value = inject_field_if_missing(body_value, "taskId", task_id);
429 let config: a2a_protocol_types::push::TaskPushNotificationConfig =
430 match serde_json::from_value(body_value) {
431 Ok(c) => c,
432 Err(e) => return error_json_response(400, &e.to_string()),
433 };
434 match self.handler.on_set_push_config(config, Some(headers)).await {
435 Ok(result) => json_ok_response(&result),
436 Err(e) => server_error_to_response(&e),
437 }
438 }
439
440 async fn handle_get_push_config(
441 &self,
442 task_id: &str,
443 config_id: &str,
444 tenant: Option<&str>,
445 headers: &HashMap<String, String>,
446 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
447 let params = a2a_protocol_types::params::GetPushConfigParams {
448 tenant: tenant.map(str::to_owned),
449 task_id: task_id.to_owned(),
450 id: config_id.to_owned(),
451 };
452 match self.handler.on_get_push_config(params, Some(headers)).await {
453 Ok(config) => json_ok_response(&config),
454 Err(e) => server_error_to_response(&e),
455 }
456 }
457
458 async fn handle_list_push_configs(
459 &self,
460 task_id: &str,
461 tenant: Option<&str>,
462 headers: &HashMap<String, String>,
463 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
464 match self
465 .handler
466 .on_list_push_configs(task_id, tenant, Some(headers))
467 .await
468 {
469 Ok(configs) => {
470 let resp = a2a_protocol_types::responses::ListPushConfigsResponse {
471 configs,
472 next_page_token: None,
473 };
474 json_ok_response(&resp)
475 }
476 Err(e) => server_error_to_response(&e),
477 }
478 }
479
480 async fn handle_delete_push_config(
481 &self,
482 task_id: &str,
483 config_id: &str,
484 tenant: Option<&str>,
485 headers: &HashMap<String, String>,
486 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
487 let params = a2a_protocol_types::params::DeletePushConfigParams {
488 tenant: tenant.map(str::to_owned),
489 task_id: task_id.to_owned(),
490 id: config_id.to_owned(),
491 };
492 match self
493 .handler
494 .on_delete_push_config(params, Some(headers))
495 .await
496 {
497 Ok(()) => json_ok_response(&serde_json::json!({})),
498 Err(e) => server_error_to_response(&e),
499 }
500 }
501
502 async fn handle_extended_card(
503 &self,
504 headers: &HashMap<String, String>,
505 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
506 match self.handler.on_get_extended_agent_card(Some(headers)).await {
507 Ok(card) => json_ok_response(&card),
508 Err(e) => server_error_to_response(&e),
509 }
510 }
511}
512
513impl std::fmt::Debug for RestDispatcher {
514 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515 f.debug_struct("RestDispatcher").finish()
516 }
517}
518
519impl crate::serve::Dispatcher for RestDispatcher {
522 fn dispatch(
523 &self,
524 req: hyper::Request<Incoming>,
525 ) -> std::pin::Pin<
526 Box<dyn std::future::Future<Output = crate::serve::DispatchResponse> + Send + '_>,
527 > {
528 Box::pin(self.dispatch(req))
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 #[test]
537 fn rest_dispatcher_debug_format() {
538 let debug_output = "RestDispatcher";
541 assert_ne!(debug_output, "");
542 }
543
544 #[test]
545 fn dispatch_config_default_query_limit() {
546 let config = super::super::DispatchConfig::default();
547 assert_eq!(config.max_query_string_length, 4096);
548 }
549}