a2a_protocol_server/handler/
mod.rs1mod capability;
23mod event_processing;
24mod helpers;
25mod lifecycle;
26mod limits;
27mod messaging;
28mod push_config;
29mod shutdown;
30
31use std::collections::HashMap;
32use std::sync::Arc;
33use std::time::{Duration, Instant};
34
35use a2a_protocol_types::agent_card::AgentCard;
36use a2a_protocol_types::task::TaskId;
37
38use crate::error::ServerResult;
39use crate::executor::AgentExecutor;
40use crate::interceptor::ServerInterceptorChain;
41use crate::metrics::Metrics;
42use crate::push::{PushConfigStore, PushSender};
43use crate::store::TaskStore;
44use crate::streaming::{EventQueueManager, InMemoryQueueReader};
45use crate::tenant_config::PerTenantConfig;
46use crate::tenant_resolver::TenantResolver;
47
48pub use limits::HandlerLimits;
49
50pub use a2a_protocol_types::responses::SendMessageResponse;
52
53pub struct RequestHandler {
68 pub(crate) executor: Arc<dyn AgentExecutor>,
69 pub(crate) task_store: Arc<dyn TaskStore>,
70 pub(crate) push_config_store: Arc<dyn PushConfigStore>,
71 pub(crate) push_sender: Option<Arc<dyn PushSender>>,
72 pub(crate) event_queue_manager: EventQueueManager,
73 pub(crate) interceptors: ServerInterceptorChain,
74 pub(crate) agent_card: Option<AgentCard>,
75 pub(crate) executor_timeout: Option<Duration>,
76 pub(crate) metrics: Arc<dyn Metrics>,
77 pub(crate) limits: HandlerLimits,
78 pub(crate) tenant_resolver: Option<Arc<dyn TenantResolver>>,
79 pub(crate) tenant_config: Option<PerTenantConfig>,
80 pub(crate) require_resolved_tenant: bool,
84 pub(crate) allow_unauthenticated_extended_card: bool,
89 pub(crate) required_extensions: Vec<String>,
94 pub(crate) declared_extensions: Vec<String>,
97 pub(crate) cancellation_tokens: Arc<tokio::sync::RwLock<HashMap<TaskId, CancellationEntry>>>,
99 pub(crate) context_locks:
103 Arc<tokio::sync::RwLock<HashMap<String, Arc<tokio::sync::Mutex<()>>>>>,
104}
105
106#[derive(Debug, Clone)]
108pub(crate) struct CancellationEntry {
109 pub(crate) token: tokio_util::sync::CancellationToken,
111 pub(crate) created_at: Instant,
113}
114
115impl RequestHandler {
116 #[must_use]
121 pub fn tenant_resolver(&self) -> Option<&dyn TenantResolver> {
122 self.tenant_resolver.as_deref()
123 }
124
125 #[must_use]
130 pub const fn tenant_config(&self) -> Option<&PerTenantConfig> {
131 self.tenant_config.as_ref()
132 }
133
134 pub(crate) async fn resolve_tenant(
155 &self,
156 method: &str,
157 headers: Option<&HashMap<String, String>>,
158 client_tenant: Option<&str>,
159 ) -> ServerResult<String> {
160 let Some(resolver) = self.tenant_resolver.as_deref() else {
161 return Ok(client_tenant.unwrap_or_default().to_owned());
162 };
163 let call_ctx = crate::handler::helpers::build_call_context(method, headers);
164 let derived = resolver.resolve(&call_ctx).await;
165 if self.require_resolved_tenant && derived.is_none() {
171 return Err(crate::error::ServerError::InvalidParams(
172 "no tenant could be determined for this request and strict \
173 multi-tenancy is enabled"
174 .to_owned(),
175 ));
176 }
177 let authoritative = derived.unwrap_or_default();
178 if let Some(client) = client_tenant {
179 if !client.is_empty() && client != authoritative {
180 return Err(crate::error::ServerError::InvalidParams(format!(
181 "request tenant '{client}' does not match the authenticated tenant"
182 )));
183 }
184 }
185 Ok(authoritative)
186 }
187}
188
189impl std::fmt::Debug for RequestHandler {
190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191 f.debug_struct("RequestHandler")
192 .field("push_sender", &self.push_sender.is_some())
193 .field("event_queue_manager", &self.event_queue_manager)
194 .field("interceptors", &self.interceptors)
195 .field("agent_card", &self.agent_card.is_some())
196 .field("metrics", &"<dyn Metrics>")
197 .field("tenant_resolver", &self.tenant_resolver.is_some())
198 .field("tenant_config", &self.tenant_config)
199 .finish_non_exhaustive()
200 }
201}
202
203#[derive(Debug)]
205#[allow(clippy::large_enum_variant)]
206pub enum SendMessageResult {
207 Response(SendMessageResponse),
209 Stream(InMemoryQueueReader),
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use crate::agent_executor;
217 use crate::builder::RequestHandlerBuilder;
218 use crate::tenant_config::{PerTenantConfig, TenantLimits};
219 use crate::tenant_resolver::HeaderTenantResolver;
220
221 struct DummyExecutor;
222 agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
223
224 #[test]
227 fn default_build_has_no_tenant_resolver() {
228 let handler = RequestHandlerBuilder::new(DummyExecutor)
229 .build()
230 .expect("default build should succeed");
231 assert!(
232 handler.tenant_resolver().is_none(),
233 "default handler should have no tenant resolver"
234 );
235 }
236
237 #[test]
238 fn default_build_has_no_tenant_config() {
239 let handler = RequestHandlerBuilder::new(DummyExecutor)
240 .build()
241 .expect("default build should succeed");
242 assert!(
243 handler.tenant_config().is_none(),
244 "default handler should have no tenant config"
245 );
246 }
247
248 #[test]
251 fn tenant_resolver_returns_some_when_configured() {
252 let handler = RequestHandlerBuilder::new(DummyExecutor)
253 .with_tenant_resolver(HeaderTenantResolver::default())
254 .build()
255 .expect("build with tenant resolver");
256 assert!(
257 handler.tenant_resolver().is_some(),
258 "should return Some when a resolver was configured"
259 );
260 }
261
262 fn headers_with(tenant: &str) -> HashMap<String, String> {
270 let mut h = HashMap::new();
271 h.insert("x-tenant-id".to_owned(), tenant.to_owned());
272 h
273 }
274
275 #[tokio::test]
276 async fn resolve_tenant_uses_resolver_when_client_omits_tenant() {
277 let handler = RequestHandlerBuilder::new(DummyExecutor)
278 .with_tenant_resolver(HeaderTenantResolver::default())
279 .build()
280 .unwrap();
281 let headers = headers_with("acme");
282 let tenant = handler
283 .resolve_tenant("GetTask", Some(&headers), None)
284 .await
285 .expect("resolution should succeed");
286 assert_eq!(tenant, "acme", "resolver-derived tenant must be used");
287 }
288
289 #[tokio::test]
290 async fn resolve_tenant_rejects_client_tenant_mismatch() {
291 let handler = RequestHandlerBuilder::new(DummyExecutor)
292 .with_tenant_resolver(HeaderTenantResolver::default())
293 .build()
294 .unwrap();
295 let headers = headers_with("acme");
297 let result = handler
298 .resolve_tenant("GetTask", Some(&headers), Some("victim"))
299 .await;
300 assert!(
301 matches!(result, Err(crate::error::ServerError::InvalidParams(_))),
302 "a client tenant disagreeing with the resolver must be rejected, got {result:?}"
303 );
304 }
305
306 #[tokio::test]
307 async fn resolve_tenant_accepts_matching_client_tenant() {
308 let handler = RequestHandlerBuilder::new(DummyExecutor)
309 .with_tenant_resolver(HeaderTenantResolver::default())
310 .build()
311 .unwrap();
312 let headers = headers_with("acme");
313 let tenant = handler
314 .resolve_tenant("GetTask", Some(&headers), Some("acme"))
315 .await
316 .expect("matching client tenant is fine");
317 assert_eq!(tenant, "acme");
318 }
319
320 #[tokio::test]
321 async fn resolve_tenant_default_falls_back_to_empty_when_unresolved() {
322 let handler = RequestHandlerBuilder::new(DummyExecutor)
325 .with_tenant_resolver(HeaderTenantResolver::default())
326 .build()
327 .unwrap();
328 let tenant = handler
329 .resolve_tenant("GetTask", None, None)
330 .await
331 .expect("default mode tolerates an unresolved tenant");
332 assert_eq!(
333 tenant, "",
334 "unresolved tenant defaults to the shared partition"
335 );
336 }
337
338 #[tokio::test]
339 async fn resolve_tenant_strict_rejects_unresolved() {
340 let handler = RequestHandlerBuilder::new(DummyExecutor)
343 .with_tenant_resolver(HeaderTenantResolver::default())
344 .require_resolved_tenant()
345 .build()
346 .unwrap();
347 let result = handler.resolve_tenant("GetTask", None, None).await;
348 assert!(
349 matches!(result, Err(crate::error::ServerError::InvalidParams(_))),
350 "strict mode must reject an unresolved tenant, got {result:?}"
351 );
352 }
353
354 #[tokio::test]
355 async fn resolve_tenant_without_resolver_trusts_client_value() {
356 let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
358 let tenant = handler
359 .resolve_tenant("GetTask", None, Some("whatever"))
360 .await
361 .unwrap();
362 assert_eq!(tenant, "whatever");
363 }
364
365 #[test]
366 fn tenant_resolver_returns_none_when_not_configured() {
367 let handler = RequestHandlerBuilder::new(DummyExecutor)
368 .build()
369 .expect("default build");
370 assert!(
371 handler.tenant_resolver().is_none(),
372 "should return None when no resolver was configured"
373 );
374 }
375
376 #[test]
379 fn tenant_config_returns_some_when_configured() {
380 let config = PerTenantConfig::builder()
381 .default_limits(TenantLimits::builder().rate_limit_rps(50).build())
382 .build();
383
384 let handler = RequestHandlerBuilder::new(DummyExecutor)
385 .with_tenant_config(config)
386 .build()
387 .expect("build with tenant config");
388 assert!(
389 handler.tenant_config().is_some(),
390 "should return Some when tenant config was provided"
391 );
392 }
393
394 #[test]
395 fn tenant_config_returns_none_when_not_configured() {
396 let handler = RequestHandlerBuilder::new(DummyExecutor)
397 .build()
398 .expect("default build");
399 assert!(
400 handler.tenant_config().is_none(),
401 "should return None when no tenant config was provided"
402 );
403 }
404
405 #[test]
406 fn tenant_config_preserves_values() {
407 let config = PerTenantConfig::builder()
408 .default_limits(TenantLimits::builder().rate_limit_rps(100).build())
409 .with_override("vip", TenantLimits::builder().rate_limit_rps(500).build())
410 .build();
411
412 let handler = RequestHandlerBuilder::new(DummyExecutor)
413 .with_tenant_config(config)
414 .build()
415 .expect("build with per-tenant overrides");
416
417 let cfg = handler.tenant_config().expect("config should be Some");
418 assert_eq!(cfg.get("vip").rate_limit_rps, Some(500));
419 assert_eq!(cfg.get("unknown-tenant").rate_limit_rps, Some(100));
420 }
421
422 #[test]
425 fn handler_with_both_tenant_fields() {
426 let handler = RequestHandlerBuilder::new(DummyExecutor)
427 .with_tenant_resolver(HeaderTenantResolver::default())
428 .with_tenant_config(
429 PerTenantConfig::builder()
430 .default_limits(TenantLimits::builder().rate_limit_rps(10).build())
431 .build(),
432 )
433 .build()
434 .expect("build with both tenant resolver and config");
435
436 assert!(handler.tenant_resolver().is_some());
437 assert!(handler.tenant_config().is_some());
438 }
439
440 #[test]
443 fn debug_impl_does_not_panic() {
444 let handler = RequestHandlerBuilder::new(DummyExecutor)
445 .build()
446 .expect("default build");
447 let debug = format!("{handler:?}");
448 assert!(
449 debug.contains("RequestHandler"),
450 "Debug output should contain struct name"
451 );
452 }
453
454 #[test]
455 fn debug_shows_tenant_resolver_presence() {
456 let without = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
457 let with = RequestHandlerBuilder::new(DummyExecutor)
458 .with_tenant_resolver(HeaderTenantResolver::default())
459 .build()
460 .unwrap();
461
462 let dbg_without = format!("{without:?}");
463 let dbg_with = format!("{with:?}");
464
465 assert!(
466 dbg_without.contains("tenant_resolver: false"),
467 "should show false when no resolver: {dbg_without}"
468 );
469 assert!(
470 dbg_with.contains("tenant_resolver: true"),
471 "should show true when resolver configured: {dbg_with}"
472 );
473 }
474
475 #[test]
478 fn send_message_result_response_variant() {
479 use a2a_protocol_types::responses::SendMessageResponse;
480 use a2a_protocol_types::task::{Task, TaskState, TaskStatus};
481
482 let task = Task {
483 id: "t1".into(),
484 context_id: "c1".into(),
485 status: TaskStatus::new(TaskState::Completed),
486 artifacts: None,
487 history: None,
488 metadata: None,
489 };
490 let result = SendMessageResult::Response(SendMessageResponse::Task(task));
491 assert!(matches!(result, SendMessageResult::Response(_)));
492 }
493}