a2a_protocol_server/dispatch/
mod.rs1#[cfg(feature = "axum")]
9pub mod axum_adapter;
10pub mod cors;
11#[cfg(feature = "grpc")]
12pub mod grpc;
13pub mod jsonrpc;
14pub mod rest;
15#[cfg(feature = "websocket")]
16pub mod websocket;
17
18pub use cors::CorsConfig;
19#[cfg(feature = "grpc")]
20pub use grpc::{GrpcConfig, GrpcDispatcher};
21pub use jsonrpc::JsonRpcDispatcher;
22pub use rest::RestDispatcher;
23#[cfg(feature = "websocket")]
24pub use websocket::WebSocketDispatcher;
25
26#[derive(Debug, Clone)]
46#[non_exhaustive]
47pub struct DispatchConfig {
48 pub max_request_body_size: usize,
50 pub body_read_timeout: std::time::Duration,
52 pub max_query_string_length: usize,
61 pub sse_keep_alive_interval: std::time::Duration,
66 pub sse_channel_capacity: usize,
71 pub max_batch_size: usize,
76 pub require_version_header: bool,
86}
87
88impl Default for DispatchConfig {
89 fn default() -> Self {
90 Self {
91 max_request_body_size: 4 * 1024 * 1024,
92 body_read_timeout: std::time::Duration::from_secs(30),
93 max_query_string_length: 4096,
94 sse_keep_alive_interval: std::time::Duration::from_secs(30),
95 sse_channel_capacity: 64,
96 max_batch_size: 100,
97 require_version_header: true,
98 }
99 }
100}
101
102impl DispatchConfig {
103 #[must_use]
105 pub const fn with_max_request_body_size(mut self, size: usize) -> Self {
106 self.max_request_body_size = size;
107 self
108 }
109
110 #[must_use]
112 pub const fn with_body_read_timeout(mut self, timeout: std::time::Duration) -> Self {
113 self.body_read_timeout = timeout;
114 self
115 }
116
117 #[must_use]
119 pub const fn with_max_query_string_length(mut self, length: usize) -> Self {
120 self.max_query_string_length = length;
121 self
122 }
123
124 #[must_use]
126 pub const fn with_sse_keep_alive_interval(mut self, interval: std::time::Duration) -> Self {
127 self.sse_keep_alive_interval = interval;
128 self
129 }
130
131 #[must_use]
133 pub const fn with_sse_channel_capacity(mut self, capacity: usize) -> Self {
134 self.sse_channel_capacity = capacity;
135 self
136 }
137
138 #[must_use]
140 pub const fn with_max_batch_size(mut self, size: usize) -> Self {
141 self.max_batch_size = size;
142 self
143 }
144
145 #[must_use]
150 pub const fn with_require_version_header(mut self, require: bool) -> Self {
151 self.require_version_header = require;
152 self
153 }
154
155 #[must_use]
163 pub const fn accept_missing_version_header(mut self) -> Self {
164 self.require_version_header = false;
165 self
166 }
167}
168
169pub const A2A_VERSION_METADATA_KEY: &str = "a2a-version";
179
180pub fn validate_version_metadata<S: std::hash::BuildHasher>(
222 metadata: &std::collections::HashMap<String, String, S>,
223 require: bool,
224) -> Result<(), a2a_protocol_types::error::A2aError> {
225 let value = metadata
226 .iter()
227 .find(|(k, _)| k.eq_ignore_ascii_case(A2A_VERSION_METADATA_KEY))
228 .map(|(_, v)| v.as_str());
229 validate_version_header(value, require)
230}
231
232pub(crate) fn validate_version_header(
241 value: Option<&str>,
242 require: bool,
243) -> Result<(), a2a_protocol_types::error::A2aError> {
244 let v = value.unwrap_or("").trim();
245 if v.is_empty() {
246 if require {
247 return Err(a2a_protocol_types::error::A2aError::version_not_supported(
248 "A2A version '0.3' is not supported by this server; expected '1.0' (send the A2A-Version header)",
249 ));
250 }
251 return Ok(());
252 }
253 let major = v.split('.').next().and_then(|s| s.parse::<u32>().ok());
254 if major == Some(1) {
255 return Ok(());
256 }
257 Err(a2a_protocol_types::error::A2aError::version_not_supported(
258 format!("unsupported A2A version: {v}; this server supports 1.x"),
259 ))
260}
261
262pub(crate) fn add_auth_challenge(headers: &mut hyper::HeaderMap, err: &crate::error::ServerError) {
266 let Some(challenge) = err.challenge() else {
267 return;
268 };
269 let value = hyper::header::HeaderValue::from_str(challenge).unwrap_or_else(|_| {
273 trace_warn!(%challenge, "auth challenge is not a valid header value; sending `Bearer`");
274 hyper::header::HeaderValue::from_static("Bearer")
275 });
276 headers.insert(hyper::header::WWW_AUTHENTICATE, value);
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use std::time::Duration;
283
284 #[test]
285 fn default_values() {
286 let config = DispatchConfig::default();
287 assert_eq!(config.max_request_body_size, 4 * 1024 * 1024);
288 assert_eq!(config.body_read_timeout, Duration::from_secs(30));
289 assert_eq!(config.max_query_string_length, 4096);
290 assert_eq!(config.sse_keep_alive_interval, Duration::from_secs(30));
291 assert_eq!(config.sse_channel_capacity, 64);
292 assert_eq!(config.max_batch_size, 100);
293 }
294
295 #[test]
296 fn with_max_request_body_size_sets_value() {
297 let config = DispatchConfig::default().with_max_request_body_size(8 * 1024 * 1024);
298 assert_eq!(config.max_request_body_size, 8 * 1024 * 1024);
299 }
300
301 #[test]
302 fn with_body_read_timeout_sets_value() {
303 let config = DispatchConfig::default().with_body_read_timeout(Duration::from_secs(60));
304 assert_eq!(config.body_read_timeout, Duration::from_secs(60));
305 }
306
307 #[test]
308 fn with_max_query_string_length_sets_value() {
309 let config = DispatchConfig::default().with_max_query_string_length(8192);
310 assert_eq!(config.max_query_string_length, 8192);
311 }
312
313 #[test]
314 fn with_sse_keep_alive_interval_sets_value() {
315 let config =
316 DispatchConfig::default().with_sse_keep_alive_interval(Duration::from_secs(15));
317 assert_eq!(config.sse_keep_alive_interval, Duration::from_secs(15));
318 }
319
320 #[test]
321 fn with_sse_channel_capacity_sets_value() {
322 let config = DispatchConfig::default().with_sse_channel_capacity(128);
323 assert_eq!(config.sse_channel_capacity, 128);
324 }
325
326 #[test]
327 fn with_max_batch_size_sets_value() {
328 let config = DispatchConfig::default().with_max_batch_size(50);
329 assert_eq!(config.max_batch_size, 50);
330 }
331
332 #[test]
333 fn builder_chaining() {
334 let config = DispatchConfig::default()
335 .with_max_request_body_size(1024)
336 .with_body_read_timeout(Duration::from_secs(10))
337 .with_max_query_string_length(2048)
338 .with_sse_keep_alive_interval(Duration::from_secs(5))
339 .with_sse_channel_capacity(32)
340 .with_max_batch_size(25);
341
342 assert_eq!(config.max_request_body_size, 1024);
343 assert_eq!(config.body_read_timeout, Duration::from_secs(10));
344 assert_eq!(config.max_query_string_length, 2048);
345 assert_eq!(config.sse_keep_alive_interval, Duration::from_secs(5));
346 assert_eq!(config.sse_channel_capacity, 32);
347 assert_eq!(config.max_batch_size, 25);
348 }
349
350 #[test]
351 fn debug_format() {
352 let config = DispatchConfig::default();
353 let debug = format!("{config:?}");
354 assert!(debug.contains("DispatchConfig"));
355 assert!(debug.contains("max_request_body_size"));
356 assert!(debug.contains("body_read_timeout"));
357 assert!(debug.contains("max_query_string_length"));
358 assert!(debug.contains("sse_keep_alive_interval"));
359 assert!(debug.contains("sse_channel_capacity"));
360 assert!(debug.contains("max_batch_size"));
361 }
362
363 #[test]
366 fn version_metadata_matches_key_case_insensitively() {
367 for key in ["a2a-version", "A2A-Version", "A2A-VERSION", "a2a-Version"] {
371 let mut md = std::collections::HashMap::new();
372 md.insert(key.to_string(), "1.0".to_string());
373 assert!(
374 validate_version_metadata(&md, true).is_ok(),
375 "key spelling {key} should be recognised"
376 );
377 }
378 }
379
380 #[test]
381 fn version_metadata_rejects_unsupported_version() {
382 let mut md = std::collections::HashMap::new();
383 md.insert("a2a-version".to_string(), "0.3".to_string());
384 let err = validate_version_metadata(&md, true).expect_err("0.3 is not supported");
385 assert!(
386 err.message.contains("0.3"),
387 "the error should name the version it rejected, got: {}",
388 err.message
389 );
390 }
391
392 #[test]
393 fn version_metadata_accepts_any_1x_including_patch() {
394 for v in ["1.0", "1.4", "1.0.2", " 1.0 "] {
395 let mut md = std::collections::HashMap::new();
396 md.insert("a2a-version".to_string(), v.to_string());
397 assert!(
398 validate_version_metadata(&md, true).is_ok(),
399 "{v} is a 1.x version and should be accepted"
400 );
401 }
402 }
403
404 #[test]
405 fn version_metadata_absent_follows_the_require_flag() {
406 let empty = std::collections::HashMap::new();
407 assert!(
408 validate_version_metadata(&empty, false).is_ok(),
409 "require=false is the gRPC posture: absent means a legacy client, accept it"
410 );
411 assert!(
412 validate_version_metadata(&empty, true).is_err(),
413 "require=true is the HTTP posture: absent means 0.3 per 3.6.2, reject it"
414 );
415 }
416
417 #[test]
418 fn version_metadata_treats_empty_value_as_absent() {
419 let mut md = std::collections::HashMap::new();
422 md.insert("a2a-version".to_string(), " ".to_string());
423 assert!(validate_version_metadata(&md, true).is_err());
424 assert!(validate_version_metadata(&md, false).is_ok());
425 }
426
427 #[test]
428 fn version_metadata_ignores_unrelated_keys() {
429 let mut md = std::collections::HashMap::new();
430 md.insert("authorization".to_string(), "Bearer x".to_string());
431 md.insert("x-tenant-id".to_string(), "acme".to_string());
432 assert!(
433 validate_version_metadata(&md, false).is_ok(),
434 "no version key present, and require=false accepts that"
435 );
436 }
437
438 #[test]
439 fn with_require_version_header_sets_the_field() {
440 let default = DispatchConfig::default().require_version_header;
441 let cfg = DispatchConfig::default().with_require_version_header(!default);
442 assert_eq!(cfg.require_version_header, !default);
443 }
444}