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)]
42pub struct DispatchConfig {
43 pub max_request_body_size: usize,
45 pub body_read_timeout: std::time::Duration,
47 pub max_query_string_length: usize,
56 pub sse_keep_alive_interval: std::time::Duration,
61 pub sse_channel_capacity: usize,
66 pub max_batch_size: usize,
71 pub require_version_header: bool,
81}
82
83impl Default for DispatchConfig {
84 fn default() -> Self {
85 Self {
86 max_request_body_size: 4 * 1024 * 1024,
87 body_read_timeout: std::time::Duration::from_secs(30),
88 max_query_string_length: 4096,
89 sse_keep_alive_interval: std::time::Duration::from_secs(30),
90 sse_channel_capacity: 64,
91 max_batch_size: 100,
92 require_version_header: true,
93 }
94 }
95}
96
97impl DispatchConfig {
98 #[must_use]
100 pub const fn with_max_request_body_size(mut self, size: usize) -> Self {
101 self.max_request_body_size = size;
102 self
103 }
104
105 #[must_use]
107 pub const fn with_body_read_timeout(mut self, timeout: std::time::Duration) -> Self {
108 self.body_read_timeout = timeout;
109 self
110 }
111
112 #[must_use]
114 pub const fn with_max_query_string_length(mut self, length: usize) -> Self {
115 self.max_query_string_length = length;
116 self
117 }
118
119 #[must_use]
121 pub const fn with_sse_keep_alive_interval(mut self, interval: std::time::Duration) -> Self {
122 self.sse_keep_alive_interval = interval;
123 self
124 }
125
126 #[must_use]
128 pub const fn with_sse_channel_capacity(mut self, capacity: usize) -> Self {
129 self.sse_channel_capacity = capacity;
130 self
131 }
132
133 #[must_use]
135 pub const fn with_max_batch_size(mut self, size: usize) -> Self {
136 self.max_batch_size = size;
137 self
138 }
139
140 #[must_use]
148 pub const fn accept_missing_version_header(mut self) -> Self {
149 self.require_version_header = false;
150 self
151 }
152}
153
154pub const A2A_VERSION_METADATA_KEY: &str = "a2a-version";
164
165pub fn validate_version_metadata<S: std::hash::BuildHasher>(
207 metadata: &std::collections::HashMap<String, String, S>,
208 require: bool,
209) -> Result<(), a2a_protocol_types::error::A2aError> {
210 let value = metadata
211 .iter()
212 .find(|(k, _)| k.eq_ignore_ascii_case(A2A_VERSION_METADATA_KEY))
213 .map(|(_, v)| v.as_str());
214 validate_version_header(value, require)
215}
216
217pub(crate) fn validate_version_header(
226 value: Option<&str>,
227 require: bool,
228) -> Result<(), a2a_protocol_types::error::A2aError> {
229 let v = value.unwrap_or("").trim();
230 if v.is_empty() {
231 if require {
232 return Err(a2a_protocol_types::error::A2aError::version_not_supported(
233 "A2A version '0.3' is not supported by this server; expected '1.0' (send the A2A-Version header)",
234 ));
235 }
236 return Ok(());
237 }
238 let major = v.split('.').next().and_then(|s| s.parse::<u32>().ok());
239 if major == Some(1) {
240 return Ok(());
241 }
242 Err(a2a_protocol_types::error::A2aError::version_not_supported(
243 format!("unsupported A2A version: {v}; this server supports 1.x"),
244 ))
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use std::time::Duration;
251
252 #[test]
253 fn default_values() {
254 let config = DispatchConfig::default();
255 assert_eq!(config.max_request_body_size, 4 * 1024 * 1024);
256 assert_eq!(config.body_read_timeout, Duration::from_secs(30));
257 assert_eq!(config.max_query_string_length, 4096);
258 assert_eq!(config.sse_keep_alive_interval, Duration::from_secs(30));
259 assert_eq!(config.sse_channel_capacity, 64);
260 assert_eq!(config.max_batch_size, 100);
261 }
262
263 #[test]
264 fn with_max_request_body_size_sets_value() {
265 let config = DispatchConfig::default().with_max_request_body_size(8 * 1024 * 1024);
266 assert_eq!(config.max_request_body_size, 8 * 1024 * 1024);
267 }
268
269 #[test]
270 fn with_body_read_timeout_sets_value() {
271 let config = DispatchConfig::default().with_body_read_timeout(Duration::from_secs(60));
272 assert_eq!(config.body_read_timeout, Duration::from_secs(60));
273 }
274
275 #[test]
276 fn with_max_query_string_length_sets_value() {
277 let config = DispatchConfig::default().with_max_query_string_length(8192);
278 assert_eq!(config.max_query_string_length, 8192);
279 }
280
281 #[test]
282 fn with_sse_keep_alive_interval_sets_value() {
283 let config =
284 DispatchConfig::default().with_sse_keep_alive_interval(Duration::from_secs(15));
285 assert_eq!(config.sse_keep_alive_interval, Duration::from_secs(15));
286 }
287
288 #[test]
289 fn with_sse_channel_capacity_sets_value() {
290 let config = DispatchConfig::default().with_sse_channel_capacity(128);
291 assert_eq!(config.sse_channel_capacity, 128);
292 }
293
294 #[test]
295 fn with_max_batch_size_sets_value() {
296 let config = DispatchConfig::default().with_max_batch_size(50);
297 assert_eq!(config.max_batch_size, 50);
298 }
299
300 #[test]
301 fn builder_chaining() {
302 let config = DispatchConfig::default()
303 .with_max_request_body_size(1024)
304 .with_body_read_timeout(Duration::from_secs(10))
305 .with_max_query_string_length(2048)
306 .with_sse_keep_alive_interval(Duration::from_secs(5))
307 .with_sse_channel_capacity(32)
308 .with_max_batch_size(25);
309
310 assert_eq!(config.max_request_body_size, 1024);
311 assert_eq!(config.body_read_timeout, Duration::from_secs(10));
312 assert_eq!(config.max_query_string_length, 2048);
313 assert_eq!(config.sse_keep_alive_interval, Duration::from_secs(5));
314 assert_eq!(config.sse_channel_capacity, 32);
315 assert_eq!(config.max_batch_size, 25);
316 }
317
318 #[test]
319 fn debug_format() {
320 let config = DispatchConfig::default();
321 let debug = format!("{config:?}");
322 assert!(debug.contains("DispatchConfig"));
323 assert!(debug.contains("max_request_body_size"));
324 assert!(debug.contains("body_read_timeout"));
325 assert!(debug.contains("max_query_string_length"));
326 assert!(debug.contains("sse_keep_alive_interval"));
327 assert!(debug.contains("sse_channel_capacity"));
328 assert!(debug.contains("max_batch_size"));
329 }
330
331 #[test]
334 fn version_metadata_matches_key_case_insensitively() {
335 for key in ["a2a-version", "A2A-Version", "A2A-VERSION", "a2a-Version"] {
339 let mut md = std::collections::HashMap::new();
340 md.insert(key.to_string(), "1.0".to_string());
341 assert!(
342 validate_version_metadata(&md, true).is_ok(),
343 "key spelling {key} should be recognised"
344 );
345 }
346 }
347
348 #[test]
349 fn version_metadata_rejects_unsupported_version() {
350 let mut md = std::collections::HashMap::new();
351 md.insert("a2a-version".to_string(), "0.3".to_string());
352 let err = validate_version_metadata(&md, true).expect_err("0.3 is not supported");
353 assert!(
354 err.message.contains("0.3"),
355 "the error should name the version it rejected, got: {}",
356 err.message
357 );
358 }
359
360 #[test]
361 fn version_metadata_accepts_any_1x_including_patch() {
362 for v in ["1.0", "1.4", "1.0.2", " 1.0 "] {
363 let mut md = std::collections::HashMap::new();
364 md.insert("a2a-version".to_string(), v.to_string());
365 assert!(
366 validate_version_metadata(&md, true).is_ok(),
367 "{v} is a 1.x version and should be accepted"
368 );
369 }
370 }
371
372 #[test]
373 fn version_metadata_absent_follows_the_require_flag() {
374 let empty = std::collections::HashMap::new();
375 assert!(
376 validate_version_metadata(&empty, false).is_ok(),
377 "require=false is the gRPC posture: absent means a legacy client, accept it"
378 );
379 assert!(
380 validate_version_metadata(&empty, true).is_err(),
381 "require=true is the HTTP posture: absent means 0.3 per 3.6.2, reject it"
382 );
383 }
384
385 #[test]
386 fn version_metadata_treats_empty_value_as_absent() {
387 let mut md = std::collections::HashMap::new();
390 md.insert("a2a-version".to_string(), " ".to_string());
391 assert!(validate_version_metadata(&md, true).is_err());
392 assert!(validate_version_metadata(&md, false).is_ok());
393 }
394
395 #[test]
396 fn version_metadata_ignores_unrelated_keys() {
397 let mut md = std::collections::HashMap::new();
398 md.insert("authorization".to_string(), "Bearer x".to_string());
399 md.insert("x-tenant-id".to_string(), "acme".to_string());
400 assert!(
401 validate_version_metadata(&md, false).is_ok(),
402 "no version key present, and require=false accepts that"
403 );
404 }
405}