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
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use std::time::Duration;
266
267 #[test]
268 fn default_values() {
269 let config = DispatchConfig::default();
270 assert_eq!(config.max_request_body_size, 4 * 1024 * 1024);
271 assert_eq!(config.body_read_timeout, Duration::from_secs(30));
272 assert_eq!(config.max_query_string_length, 4096);
273 assert_eq!(config.sse_keep_alive_interval, Duration::from_secs(30));
274 assert_eq!(config.sse_channel_capacity, 64);
275 assert_eq!(config.max_batch_size, 100);
276 }
277
278 #[test]
279 fn with_max_request_body_size_sets_value() {
280 let config = DispatchConfig::default().with_max_request_body_size(8 * 1024 * 1024);
281 assert_eq!(config.max_request_body_size, 8 * 1024 * 1024);
282 }
283
284 #[test]
285 fn with_body_read_timeout_sets_value() {
286 let config = DispatchConfig::default().with_body_read_timeout(Duration::from_secs(60));
287 assert_eq!(config.body_read_timeout, Duration::from_secs(60));
288 }
289
290 #[test]
291 fn with_max_query_string_length_sets_value() {
292 let config = DispatchConfig::default().with_max_query_string_length(8192);
293 assert_eq!(config.max_query_string_length, 8192);
294 }
295
296 #[test]
297 fn with_sse_keep_alive_interval_sets_value() {
298 let config =
299 DispatchConfig::default().with_sse_keep_alive_interval(Duration::from_secs(15));
300 assert_eq!(config.sse_keep_alive_interval, Duration::from_secs(15));
301 }
302
303 #[test]
304 fn with_sse_channel_capacity_sets_value() {
305 let config = DispatchConfig::default().with_sse_channel_capacity(128);
306 assert_eq!(config.sse_channel_capacity, 128);
307 }
308
309 #[test]
310 fn with_max_batch_size_sets_value() {
311 let config = DispatchConfig::default().with_max_batch_size(50);
312 assert_eq!(config.max_batch_size, 50);
313 }
314
315 #[test]
316 fn builder_chaining() {
317 let config = DispatchConfig::default()
318 .with_max_request_body_size(1024)
319 .with_body_read_timeout(Duration::from_secs(10))
320 .with_max_query_string_length(2048)
321 .with_sse_keep_alive_interval(Duration::from_secs(5))
322 .with_sse_channel_capacity(32)
323 .with_max_batch_size(25);
324
325 assert_eq!(config.max_request_body_size, 1024);
326 assert_eq!(config.body_read_timeout, Duration::from_secs(10));
327 assert_eq!(config.max_query_string_length, 2048);
328 assert_eq!(config.sse_keep_alive_interval, Duration::from_secs(5));
329 assert_eq!(config.sse_channel_capacity, 32);
330 assert_eq!(config.max_batch_size, 25);
331 }
332
333 #[test]
334 fn debug_format() {
335 let config = DispatchConfig::default();
336 let debug = format!("{config:?}");
337 assert!(debug.contains("DispatchConfig"));
338 assert!(debug.contains("max_request_body_size"));
339 assert!(debug.contains("body_read_timeout"));
340 assert!(debug.contains("max_query_string_length"));
341 assert!(debug.contains("sse_keep_alive_interval"));
342 assert!(debug.contains("sse_channel_capacity"));
343 assert!(debug.contains("max_batch_size"));
344 }
345
346 #[test]
349 fn version_metadata_matches_key_case_insensitively() {
350 for key in ["a2a-version", "A2A-Version", "A2A-VERSION", "a2a-Version"] {
354 let mut md = std::collections::HashMap::new();
355 md.insert(key.to_string(), "1.0".to_string());
356 assert!(
357 validate_version_metadata(&md, true).is_ok(),
358 "key spelling {key} should be recognised"
359 );
360 }
361 }
362
363 #[test]
364 fn version_metadata_rejects_unsupported_version() {
365 let mut md = std::collections::HashMap::new();
366 md.insert("a2a-version".to_string(), "0.3".to_string());
367 let err = validate_version_metadata(&md, true).expect_err("0.3 is not supported");
368 assert!(
369 err.message.contains("0.3"),
370 "the error should name the version it rejected, got: {}",
371 err.message
372 );
373 }
374
375 #[test]
376 fn version_metadata_accepts_any_1x_including_patch() {
377 for v in ["1.0", "1.4", "1.0.2", " 1.0 "] {
378 let mut md = std::collections::HashMap::new();
379 md.insert("a2a-version".to_string(), v.to_string());
380 assert!(
381 validate_version_metadata(&md, true).is_ok(),
382 "{v} is a 1.x version and should be accepted"
383 );
384 }
385 }
386
387 #[test]
388 fn version_metadata_absent_follows_the_require_flag() {
389 let empty = std::collections::HashMap::new();
390 assert!(
391 validate_version_metadata(&empty, false).is_ok(),
392 "require=false is the gRPC posture: absent means a legacy client, accept it"
393 );
394 assert!(
395 validate_version_metadata(&empty, true).is_err(),
396 "require=true is the HTTP posture: absent means 0.3 per 3.6.2, reject it"
397 );
398 }
399
400 #[test]
401 fn version_metadata_treats_empty_value_as_absent() {
402 let mut md = std::collections::HashMap::new();
405 md.insert("a2a-version".to_string(), " ".to_string());
406 assert!(validate_version_metadata(&md, true).is_err());
407 assert!(validate_version_metadata(&md, false).is_ok());
408 }
409
410 #[test]
411 fn version_metadata_ignores_unrelated_keys() {
412 let mut md = std::collections::HashMap::new();
413 md.insert("authorization".to_string(), "Bearer x".to_string());
414 md.insert("x-tenant-id".to_string(), "acme".to_string());
415 assert!(
416 validate_version_metadata(&md, false).is_ok(),
417 "no version key present, and require=false accepts that"
418 );
419 }
420
421 #[test]
422 fn with_require_version_header_sets_the_field() {
423 let default = DispatchConfig::default().require_version_header;
424 let cfg = DispatchConfig::default().with_require_version_header(!default);
425 assert_eq!(cfg.require_version_header, !default);
426 }
427}