Demonstrates how data flows from a middleware layer into a connectrpc
handler via Context::extensions. The server stack composes a
bearer-token auth check (written as an axum::middleware::from_fn),
tower-http's TraceLayer for request logging, and TimeoutLayer for
a per-request deadline.
The handler reads the caller identity from Context::extensions() and
writes a x-served-by response trailer via Context::set_trailer().
# Terminal 1: server with INFO-level tracing
RUST_LOG=info,tower_http=debug \
cargo run -p middleware-example --bin middleware-server
# Terminal 2: client (sends auth header via ClientConfig::default_header)
cargo run -p middleware-example --bin middleware-client
# Or with curl:
curl -X POST http://127.0.0.1:8080/anthropic.connectrpc.middleware_demo.v1.SecretService/GetSecret \
-H 'authorization: Bearer demo-token-alice' \
-H 'content-type: application/json' \
-d '{"name": "shared"}'Expected client output:
shared -> the value of teamwork
trailer x-served-by: alice
alice-only -> alice's diary entry
Set MIDDLEWARE_TOKEN to demo-token-bob to see the permission-denied
path on alice-only. Set MIDDLEWARE_URL to point at a different
address.
-
auth_middleware- an async function written in axum'sfrom_fnstyle. Validates aBearer <token>header against a static map. On success, stamps aUserIdinto the request extensions and callsnext.run(req). On failure, returns a 401 directly with a Connect-protocol JSON error body. Mounted viaaxum::middleware::from_fn_with_state(tokens, auth_middleware), which is the idiomatic axum pattern for stateful auth.A hand-rolled
tower::Layer+tower::Servicepair would reach the sameContext::extensionsendpoint but requires more boilerplate. The connectrpc dispatcher only cares that something earlier in the stack inserted the value; how it got there is up to you. -
Tower stack composition -
ServiceBuilderapplies layers top-to-bottom (outermost first). Wrapped in axum'sRouter::layer()so axum handles the body conversion fromConnectRpcBodytoaxum::body::Bodyautomatically:let tokens = Arc::new(token_table()); axum::Router::new() .fallback_service(connect_router.into_axum_service()) .layer( ServiceBuilder::new() .layer(TraceLayer::new_for_http()) // outermost .layer(axum::middleware::from_fn_with_state(tokens, auth_middleware)) .layer(TimeoutLayer::with_status_code(...)), // innermost );
-
Handler reading from
Context- the dispatch path moves the request'shttp::ExtensionsintoContext::extensions. The handler readsUserIdviactx.extensions.get::<UserId>(), performs its own permission check against the secret store, and writes thex-served-byresponse trailer viactx.set_trailer(...).
ClientConfig::default_header- sets the auth header once on the client config; every RPC call picks it up automatically.ClientConfig::default_timeout- default deadline for every call.CallOptions::with_timeout- per-call deadline override._with_optionsvariants let any RPC method take per-call options.resp.trailers()- unary responses surface trailers alongside the body (and headers viaresp.headers()).
tests/e2e.rs spins up the server with the same from_fn middleware
stack and exercises four paths: authorized success (verifies the
trailer arrives), missing auth header (expects Unauthenticated),
invalid token (expects Unauthenticated), and permission denied at
the handler level (expects PermissionDenied).
cargo test -p middleware-example- See
examples/streaming-tourfor handler signatures across all four RPC types. - See
examples/elizafor tower-http CORS layered onto a streaming service plus TLS/mTLS support.