Interceptors & Middleware

Interceptors let you hook into the request/response pipeline on both the client and server side — for authentication, logging, metrics, rate limiting, or any cross-cutting concern.

Server Interceptors

Server interceptors run before and after the handler processes a request:

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::server::ServerInterceptor;

struct LoggingInterceptor;

impl ServerInterceptor for LoggingInterceptor {
    // Intercept incoming requests and outgoing responses
}
}

Adding Interceptors

#![allow(unused)]
fn main() {
RequestHandlerBuilder::new(my_executor)
    .with_interceptor(AuthInterceptor::new(auth_config))
    .with_interceptor(LoggingInterceptor)
    .with_interceptor(MetricsInterceptor::new())
    .build()
}

Interceptors execute in the order they're added:

Request → Auth → Logging → Metrics → Handler → Metrics → Logging → Auth → Response

Example: Authentication

#![allow(unused)]
fn main() {
struct BearerAuthInterceptor {
    valid_tokens: HashSet<String>,
}

impl ServerInterceptor for BearerAuthInterceptor {
    // Check Authorization header before passing to handler
    // Return 401 if token is missing or invalid
}
}

Client Interceptors

Client interceptors modify outgoing requests and incoming responses:

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::client::CallInterceptor;

struct RequestIdInterceptor;

impl CallInterceptor for RequestIdInterceptor {
    // Add X-Request-Id header to outgoing requests
    // Log the response status
}
}

Adding Client Interceptors

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::client::ClientBuilder;

let client = ClientBuilder::new("http://agent.example.com".into())
    .with_interceptor(RequestIdInterceptor)
    .with_interceptor(RetryInterceptor::new(3))
    .build()
    .unwrap();
}

Common Patterns

Logging

Log method names, durations, and errors:

#![allow(unused)]
fn main() {
struct LoggingInterceptor;
// Log: "SendMessage completed in 42ms"
// Log: "GetTask failed: task not found (15ms)"
}

Metrics

Track request counts, latencies, error rates:

#![allow(unused)]
fn main() {
struct MetricsInterceptor {
    counter: Arc<AtomicU64>,
}
// Increment counter on each request
// Record latency histogram
}

Rate Limiting

The built-in RateLimitInterceptor provides per-caller fixed-window rate limiting:

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::server::{RateLimitInterceptor, RateLimitConfig};
use std::sync::Arc;

let limiter = Arc::new(RateLimitInterceptor::new(RateLimitConfig {
    requests_per_window: 100,
    window_secs: 60,
}));

// Add to handler builder:
RequestHandlerBuilder::new(my_executor)
    .with_interceptor(limiter)
    .build()
}

Caller keys are derived from CallContext::caller_identity (set by auth interceptors), the X-Forwarded-For header, or "anonymous". For advanced use cases (sliding windows, distributed counters), implement a custom ServerInterceptor or use a reverse proxy.

Interceptor Chain

Both client and server support ordered interceptor chains. The chain is built incrementally:

#![allow(unused)]
fn main() {
// Each .with_interceptor() call appends to the chain
builder
    .with_interceptor(first)    // Runs first on request, last on response
    .with_interceptor(second)   // Runs second on request, second-to-last on response
    .with_interceptor(third)    // Runs third on request, first on response
}

Next Steps