Expand description
Axum framework integration for A2A servers.
Provides A2aRouter, which builds an axum::Router that handles all
A2A v1.0 methods using the existing RequestHandler.
§Quick start
use std::sync::Arc;
use a2a_protocol_server::dispatch::axum_adapter::A2aRouter;
use a2a_protocol_server::RequestHandlerBuilder;
let handler = Arc::new(
RequestHandlerBuilder::new(MyExecutor)
.build()
.expect("build handler"),
);
let app = A2aRouter::new(handler).into_router();
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();§Composability
The returned router can be merged with other Axum routes, middleware, and layers:
let app = axum::Router::new()
.merge(A2aRouter::new(handler).into_router())
.layer(tower_http::cors::CorsLayer::permissive())
.route("/custom", get(custom_handler));§Multi-tenancy: use a resolver, not the URL prefix
This router registers no /tenants/{tenant}/… routes, unlike the built-in
REST dispatcher (crate::dispatch::rest), which strips that prefix and
threads the tenant through. Requests to a tenant-prefixed path therefore
404 here — fail-safe, but surprising if you are porting from
serve(), where the same URL works.
Tenancy itself is not lost. This router forwards request headers to the
handler, so a configured
TenantResolver — for example
HeaderTenantResolver —
resolves tenants normally, and the resolver is authoritative over any
client-supplied value. Pair it with
require_resolved_tenant
so a request that carries no tenant is rejected rather than served from
the shared default partition.
With no resolver configured, every request through this router is
served from the default ("") tenant, because the per-request tenant
field is not populated from the URL. That is correct for single-tenant
deployments and is the reason the prefix is absent rather than silently
mis-parsed; it is called out here so it is a choice rather than a
discovery.