Loading pageâŚ
Rust walkthroughs
Loading pageâŚ
tower::Service trait enable middleware composition in async Rust applications?The tower::Service trait provides a uniform interface for asynchronous request-response operations, enabling a powerful middleware composition pattern. By abstracting over the details of how requests are processed, it allows middleware layers to be stacked and combined in ways that would be difficult with ad-hoc approaches.
At its core, the Service trait represents an asynchronous function from a request to a response:
use tower::Service;
use std::future::Future;
pub trait Service<Request> {
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>)
-> std::task::Poll<Result<(), Self::Error>>;
fn call(&mut self, req: Request) -> Self::Future;
}The trait separates readiness checking (poll_ready) from request processing (call), enabling backpressure and resource management.
A simple service wraps an async operation:
use tower::Service;
use std::future::{Future, Ready, ready};
struct EchoService;
impl Service<String> for EchoService {
type Response = String;
type Error = std::convert::Infallible;
type Future = Ready<Result<String, Self::Error>>;
fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>)
-> std::task::Poll<Result<(), Self::Error>>
{
// Always ready
std::task::Poll::Ready(Ok(()))
}
fn call(&mut self, req: String) -> Self::Future {
ready(Ok(req))
}
}This abstraction allows any async operation to be treated uniformly.
Middleware wraps an inner service, transforming requests before they reach the inner service or responses before they return:
use tower::Service;
use std::future::Future;
use std::pin::Pin;
struct LoggingService<S> {
inner: S,
}
impl<S, Request> Service<Request> for LoggingService<S>
where
S: Service<Request>,
S::Error: std::fmt::Debug,
{
type Response = S::Response;
type Error = S::Error;
type Future = LoggingFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>)
-> std::task::Poll<Result<(), Self::Error>>
{
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request) -> Self::Future {
println!("Received request");
LoggingFuture {
inner: self.inner.call(req),
}
}
}
struct LoggingFuture<F> {
inner: F,
}
impl<F, T, E> Future for LoggingFuture<F>
where
F: Future<Output = Result<T, E>>,
E: std::fmt::Debug,
{
type Output = Result<T, E>;
fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>)
-> std::task::Poll<Self::Output>
{
let result = unsafe {
self.as_mut().map_unchecked_mut(|s| &mut s.inner).poll(cx)
};
if let std::task::Poll::Ready(ref res) = result {
match res {
Ok(_) => println!("Request succeeded"),
Err(e) => println!("Request failed: {:?}", e),
}
}
result
}
}The middleware intercepts the request-response cycle without knowing the details of the inner service.
Middleware layers compose naturally by wrapping each other:
use tower::Service;
// Assume we have these middleware types:
// LoggingService<S>
// TimeoutService<S>
// RateLimitService<S>
type MyService = LoggingService<
TimeoutService<
RateLimitService<
EchoService
>
>
>;
// Request flow: Logging -> Timeout -> RateLimit -> Echo
// Response flow: Echo -> RateLimit -> Timeout -> LoggingEach layer adds behavior while delegating to the next. The type signature reflects the composition order.
Tower provides the Layer trait to separate middleware configuration from application:
use tower::Service;
pub trait Layer<S> {
type Service: Service<Request>;
fn layer(&self, inner: S) -> Self::Service;
}
// Using layers
use tower::ServiceBuilder;
let service = ServiceBuilder::new()
.layer(LoggingLayer)
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.layer(RateLimitLayer::new(100, Duration::from_secs(1)))
.service(EchoService);ServiceBuilder provides a fluent interface for stacking layers, producing a single composed service.
Tower includes many ready-to-use middleware:
use tower::ServiceBuilder;
use tower::limit::{RateLimitLayer, ConcurrencyLimitLayer};
use tower::timeout::TimeoutLayer;
use tower::retry::RetryLayer;
use tower::load_shed::LoadShedLayer;
use std::time::Duration;
let service = ServiceBuilder::new()
// Shed load when backlogged
.layer(LoadShedLayer::new())
// Limit to 100 concurrent requests
.layer(ConcurrencyLimitLayer::new(100))
// Limit to 10 requests per second
.layer(RateLimitLayer::new(10, Duration::from_millis(100)))
// Add timeout
.layer(TimeoutLayer::new(Duration::from_secs(30)))
// Retry failed requests
.layer(RetryLayer::new(retry_policy))
.service(inner_service);These layers compose correctly because they all implement Service.
Middleware can transform the request type:
use tower::Service;
use std::future::{Future, Ready, ready};
struct AddHeaderService<S> {
inner: S,
}
#[derive(Debug)]
struct RequestWithHeaders {
body: String,
headers: Vec<(String, String)>,
}
impl<S> Service<String> for AddHeaderService<S>
where
S: Service<RequestWithHeaders>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>)
-> std::task::Poll<Result<(), Self::Error>>
{
self.inner.poll_ready(cx)
}
fn call(&mut self, req: String) -> Self::Future {
let enriched = RequestWithHeaders {
body: req,
headers: vec![("X-Custom".to_string(), "value".to_string())],
};
self.inner.call(enriched)
}
}This pattern enables middleware to add context to requests as they flow through the stack.
Middleware can also transform responses:
use tower::Service;
use std::future::Future;
use std::pin::Pin;
struct MapResponseService<S, F> {
inner: S,
f: F,
}
impl<S, F, Request, Response> Service<Request> for MapResponseService<S, F>
where
S: Service<Request>,
F: Fn(S::Response) -> Response + Clone,
{
type Response = Response;
type Error = S::Error;
type Future = MapResponseFuture<S::Future, F>;
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>)
-> std::task::Poll<Result<(), Self::Error>>
{
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request) -> Self::Future {
MapResponseFuture {
inner: self.inner.call(req),
f: Some(self.f.clone()),
}
}
}
struct MapResponseFuture<Fut, F> {
inner: Fut,
f: Option<F>,
}
impl<Fut, F, Response> Future for MapResponseFuture<Fut, F>
where
Fut: Future,
F: Fn(Fut::Output) -> Response,
{
type Output = Response;
fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>)
-> std::task::Poll<Self::Output>
{
let result = unsafe {
self.as_mut().map_unchecked_mut(|s| &mut s.inner).poll(cx)
};
match result {
std::task::Poll::Ready(output) => {
let f = self.f.take().unwrap();
std::task::Poll::Ready(f(output))
}
std::task::Poll::Pending => std::task::Poll::Pending,
}
}
}This enables middleware to adapt response types or post-process results.
The poll_ready method enables backpressure propagation:
use tower::Service;
use std::sync::atomic::{AtomicUsize, Ordering};
struct ConcurrencyLimitService<S> {
inner: S,
active: AtomicUsize,
limit: usize,
}
impl<S, Request> Service<Request> for ConcurrencyLimitService<S>
where
S: Service<Request>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>)
-> std::task::Poll<Result<(), Self::Error>>
{
// Check if we're at capacity
if self.active.load(Ordering::Relaxed) >= self.limit {
// Not ready - caller should wait
cx.waker().wake_by_ref();
return std::task::Poll::Pending;
}
// Check if inner service is ready
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request) -> Self::Future {
self.active.fetch_add(1, Ordering::Relaxed);
// In a real implementation, we'd decrement on completion
self.inner.call(req)
}
}When a service isn't ready, callers should wait before invoking call. This propagates backpressure through the middleware stack.
The tower-http crate provides HTTP-specific middleware using Tower's abstractions:
use tower_http::{
trace::TraceLayer,
compression::CompressionLayer,
cors::CorsLayer,
limit::RequestBodyLimitLayer,
};
use tower::ServiceBuilder;
let service = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new())
.layer(CorsLayer::permissive())
.layer(RequestBodyLimitLayer::new(1024 * 1024)) // 1MB limit
.service(http_handler);Each layer adds HTTP-specific functionality while maintaining the Service interface.
Axum, Tonic, and other frameworks are built on Tower:
use axum::{
Router,
routing::get,
};
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
let app = Router::new()
.route("/", get(handler))
.layer(ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
// More middleware...
);
// Axum routes are Tower services
// The .layer() method adds middleware to all routesThis integration means middleware written for Tower works across multiple frameworks.
The ServiceExt trait provides utilities for working with services:
use tower::{Service, ServiceExt};
async fn example() -> Result<(), Box<dyn std::error::Error>> {
let mut service = MyService::new();
// Wait until ready
service.ready().await?;
// Call the service
let response = service.call(Request::new()).await?;
// Or use the one-shot pattern
let response = MyService::new()
.oneshot(Request::new())
.await?;
Ok(())
}Services often need to be cloned for each request:
use tower::Service;
use std::sync::Arc;
struct SharedStateService<S> {
inner: S,
state: Arc<AppState>,
}
impl<S> Clone for SharedStateService<S>
where
S: Clone,
{
fn clone(&self) -> Self {
SharedStateService {
inner: self.inner.clone(),
state: Arc::clone(&self.state),
}
}
}
// Tower's ServiceBuilder creates services that can be cloned
// Each request gets its own clone of the service stackThis pattern ensures each request has isolated service state while sharing application state through Arc.
The Service trait's power comes from uniformity. Every middleware implements the same interface, enabling arbitrary composition:
use tower::ServiceBuilder;
// This composition is possible because all layers produce Services
let service = ServiceBuilder::new()
.layer(MetricsLayer::new())
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.layer(RetryLayer::new(policy))
.layer(ConcurrencyLimitLayer::new(100))
.layer(CacheLayer::new())
.service(handler);Each layer is oblivious to what comes before or afterâit just sees a Service to delegate to.
Errors propagate through the middleware stack:
use tower::Service;
use std::future::Future;
struct ErrorHandlerService<S, F> {
inner: S,
handler: F,
}
impl<S, F, Request, Response, Error> Service<Request> for ErrorHandlerService<S, F>
where
S: Service<Request, Error = Error>,
F: Fn(Error) -> Response + Clone,
{
type Response = Response;
type Error = std::convert::Infallible; // All errors handled
type Future = ErrorHandlerFuture<S::Future, F>;
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>)
-> std::task::Poll<Result<(), Error>>
{
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request) -> Self::Future {
ErrorHandlerFuture {
inner: self.inner.call(req),
handler: Some(self.handler.clone()),
}
}
}
// Errors from inner services can be caught and transformed
// by outer middleware layersThis enables centralized error handling and recovery strategies.
The tower::Service trait enables middleware composition by providing a uniform interface for asynchronous request-response operations. Middleware wraps inner services, transforming requests and responses while delegating through the stack. The Layer trait separates middleware configuration from application, and ServiceBuilder provides a fluent interface for composition.
The separation of poll_ready from call enables backpressure propagation, allowing services to signal when they're at capacity. Combined with the uniform interface, this means middleware can implement rate limiting, timeouts, retries, and load shedding in a way that composes correctly with other middleware.
This abstraction underpins Rust's web ecosystemâAxum, Tonic, and other frameworks are built on Tower, meaning middleware written for one framework often works with others. The pattern demonstrates how a simple, well-designed trait can enable powerful composition without sacrificing type safety or performance.