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