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 = self
175 .dispatch_rest(req, method.as_str(), rest_path, &query, tenant, &headers)
176 .await;
177 if let Some(hval) = self
181 .handler
182 .activated_extensions_header_value(headers.get("a2a-extensions").map(String::as_str))
183 {
184 if let Ok(v) = hyper::header::HeaderValue::from_str(&hval) {
185 resp.headers_mut()
186 .insert(a2a_protocol_types::A2A_EXTENSIONS_HEADER, v);
187 }
188 }
189 if let Some(ref cors) = self.cors {
190 cors.apply_headers(&mut resp);
191 }
192 resp
193 }
194
195 #[allow(clippy::too_many_lines)]
197 async fn dispatch_rest(
198 &self,
199 req: hyper::Request<Incoming>,
200 method: &str,
201 path: &str,
202 query: &str,
203 tenant: Option<&str>,
204 headers: &HashMap<String, String>,
205 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
206 match (method, path) {
209 ("POST", "/message:send") => {
210 return self.handle_send(req, false, headers).await;
211 }
212 ("POST", "/message:stream") => {
213 return self.handle_send(req, true, headers).await;
214 }
215 _ => {}
216 }
217
218 if let Some(rest) = path.strip_prefix("/tasks/") {
220 if let Some((id, action)) = rest.split_once(':') {
221 if !id.is_empty() {
222 match (method, action) {
223 ("POST", "cancel") => {
224 return self.handle_cancel_task(id, tenant, headers).await;
225 }
226 ("POST" | "GET", "subscribe") => {
235 return self.handle_resubscribe(id, tenant, headers).await;
236 }
237 _ => {}
238 }
239 }
240 }
241 }
242
243 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
244
245 match (method, segments.as_slice()) {
246 ("GET", ["tasks"]) => self.handle_list_tasks(query, tenant, headers).await,
248 ("GET", ["tasks", id]) => self.handle_get_task(id, query, tenant, headers).await,
249
250 ("POST", ["tasks", id, "cancel"]) => self.handle_cancel_task(id, tenant, headers).await,
252
253 ("POST", ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig"]) => {
255 self.handle_set_push_config(req, task_id, headers).await
256 }
257 (
258 "GET",
259 ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig", config_id],
260 ) => {
261 self.handle_get_push_config(task_id, config_id, tenant, headers)
262 .await
263 }
264 ("GET", ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig"]) => {
265 self.handle_list_push_configs(task_id, tenant, headers)
266 .await
267 }
268 (
269 "DELETE",
270 ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig", config_id],
271 )
272 | (
273 "POST",
274 ["tasks", task_id, "pushNotificationConfigs" | "pushNotificationConfig", config_id, "delete"],
275 ) => {
276 self.handle_delete_push_config(task_id, config_id, tenant, headers)
277 .await
278 }
279
280 ("GET", ["extendedAgentCard"]) => self.handle_extended_card(headers).await,
282
283 _ => not_found_response(),
284 }
285 }
286
287 async fn handle_send(
290 &self,
291 req: hyper::Request<Incoming>,
292 streaming: bool,
293 headers: &HashMap<String, String>,
294 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
295 let body_bytes = match read_body_limited(
296 req.into_body(),
297 self.config.max_request_body_size,
298 self.config.body_read_timeout,
299 )
300 .await
301 {
302 Ok(bytes) => bytes,
303 Err(msg) => return error_json_response(413, &msg),
304 };
305 let params: a2a_protocol_types::params::MessageSendParams =
306 match serde_json::from_slice(&body_bytes) {
307 Ok(p) => p,
308 Err(e) => return error_json_response(400, &e.to_string()),
309 };
310 match self
311 .handler
312 .on_send_message(params, streaming, Some(headers))
313 .await
314 {
315 Ok(SendMessageResult::Response(resp)) => json_ok_response(&resp),
316 Ok(SendMessageResult::Stream(reader)) => build_sse_response(
317 reader,
318 Some(self.config.sse_keep_alive_interval),
319 Some(self.config.sse_channel_capacity),
320 None, ),
322 Err(e) => server_error_to_response(&e),
323 }
324 }
325
326 async fn handle_get_task(
327 &self,
328 id: &str,
329 query: &str,
330 tenant: Option<&str>,
331 headers: &HashMap<String, String>,
332 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
333 let history_length = parse_query_param_u32(query, "historyLength");
334 let params = a2a_protocol_types::params::TaskQueryParams {
335 tenant: tenant.map(str::to_owned),
336 id: id.to_owned(),
337 history_length,
338 };
339 match self.handler.on_get_task(params, Some(headers)).await {
340 Ok(task) => json_ok_response(&task),
341 Err(e) => server_error_to_response(&e),
342 }
343 }
344
345 async fn handle_list_tasks(
346 &self,
347 query: &str,
348 tenant: Option<&str>,
349 headers: &HashMap<String, String>,
350 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
351 let params = parse_list_tasks_query(query, tenant);
352 match self.handler.on_list_tasks(params, Some(headers)).await {
353 Ok(result) => json_ok_response(&result),
354 Err(e) => server_error_to_response(&e),
355 }
356 }
357
358 async fn handle_cancel_task(
359 &self,
360 id: &str,
361 tenant: Option<&str>,
362 headers: &HashMap<String, String>,
363 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
364 let params = a2a_protocol_types::params::CancelTaskParams {
365 tenant: tenant.map(str::to_owned),
366 id: id.to_owned(),
367 metadata: None,
368 };
369 match self.handler.on_cancel_task(params, Some(headers)).await {
370 Ok(task) => json_ok_response(&task),
371 Err(e) => server_error_to_response(&e),
372 }
373 }
374
375 async fn handle_resubscribe(
376 &self,
377 id: &str,
378 tenant: Option<&str>,
379 headers: &HashMap<String, String>,
380 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
381 let params = a2a_protocol_types::params::TaskIdParams {
382 tenant: tenant.map(str::to_owned),
383 id: id.to_owned(),
384 };
385 match self.handler.on_resubscribe(params, Some(headers)).await {
386 Ok(reader) => build_sse_response(
387 reader,
388 Some(self.config.sse_keep_alive_interval),
389 Some(self.config.sse_channel_capacity),
390 None, ),
392 Err(e) => server_error_to_response(&e),
393 }
394 }
395
396 async fn handle_set_push_config(
397 &self,
398 req: hyper::Request<Incoming>,
399 task_id: &str,
400 headers: &HashMap<String, String>,
401 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
402 let body_bytes = match read_body_limited(
403 req.into_body(),
404 self.config.max_request_body_size,
405 self.config.body_read_timeout,
406 )
407 .await
408 {
409 Ok(bytes) => bytes,
410 Err(msg) => return error_json_response(413, &msg),
411 };
412 let body_value: serde_json::Value = match serde_json::from_slice(&body_bytes) {
416 Ok(v) => v,
417 Err(e) => return error_json_response(400, &e.to_string()),
418 };
419 let body_value = inject_field_if_missing(body_value, "taskId", task_id);
420 let config: a2a_protocol_types::push::TaskPushNotificationConfig =
421 match serde_json::from_value(body_value) {
422 Ok(c) => c,
423 Err(e) => return error_json_response(400, &e.to_string()),
424 };
425 match self.handler.on_set_push_config(config, Some(headers)).await {
426 Ok(result) => json_ok_response(&result),
427 Err(e) => server_error_to_response(&e),
428 }
429 }
430
431 async fn handle_get_push_config(
432 &self,
433 task_id: &str,
434 config_id: &str,
435 tenant: Option<&str>,
436 headers: &HashMap<String, String>,
437 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
438 let params = a2a_protocol_types::params::GetPushConfigParams {
439 tenant: tenant.map(str::to_owned),
440 task_id: task_id.to_owned(),
441 id: config_id.to_owned(),
442 };
443 match self.handler.on_get_push_config(params, Some(headers)).await {
444 Ok(config) => json_ok_response(&config),
445 Err(e) => server_error_to_response(&e),
446 }
447 }
448
449 async fn handle_list_push_configs(
450 &self,
451 task_id: &str,
452 tenant: Option<&str>,
453 headers: &HashMap<String, String>,
454 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
455 match self
456 .handler
457 .on_list_push_configs(task_id, tenant, Some(headers))
458 .await
459 {
460 Ok(configs) => {
461 let resp = a2a_protocol_types::responses::ListPushConfigsResponse {
462 configs,
463 next_page_token: None,
464 };
465 json_ok_response(&resp)
466 }
467 Err(e) => server_error_to_response(&e),
468 }
469 }
470
471 async fn handle_delete_push_config(
472 &self,
473 task_id: &str,
474 config_id: &str,
475 tenant: Option<&str>,
476 headers: &HashMap<String, String>,
477 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
478 let params = a2a_protocol_types::params::DeletePushConfigParams {
479 tenant: tenant.map(str::to_owned),
480 task_id: task_id.to_owned(),
481 id: config_id.to_owned(),
482 };
483 match self
484 .handler
485 .on_delete_push_config(params, Some(headers))
486 .await
487 {
488 Ok(()) => json_ok_response(&serde_json::json!({})),
489 Err(e) => server_error_to_response(&e),
490 }
491 }
492
493 async fn handle_extended_card(
494 &self,
495 headers: &HashMap<String, String>,
496 ) -> hyper::Response<BoxBody<Bytes, Infallible>> {
497 match self.handler.on_get_extended_agent_card(Some(headers)).await {
498 Ok(card) => json_ok_response(&card),
499 Err(e) => server_error_to_response(&e),
500 }
501 }
502}
503
504impl std::fmt::Debug for RestDispatcher {
505 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506 f.debug_struct("RestDispatcher").finish()
507 }
508}
509
510impl crate::serve::Dispatcher for RestDispatcher {
513 fn dispatch(
514 &self,
515 req: hyper::Request<Incoming>,
516 ) -> std::pin::Pin<
517 Box<dyn std::future::Future<Output = crate::serve::DispatchResponse> + Send + '_>,
518 > {
519 Box::pin(self.dispatch(req))
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 #[test]
528 fn rest_dispatcher_debug_format() {
529 let debug_output = "RestDispatcher";
532 assert!(!debug_output.is_empty());
533 }
534
535 #[test]
536 fn dispatch_config_default_query_limit() {
537 let config = super::super::DispatchConfig::default();
538 assert_eq!(config.max_query_string_length, 4096);
539 }
540}