tezvyn:

Rust's Tower Service: One Trait for Clients, Servers, and Middleware

AI-drafted, machine-checkedSource: docs.rsadvanced

Tower's Service trait is a universal API for async requests. It models any 'request -> future<response>' flow, unifying clients, servers, and middleware. Use it for HTTP servers or database clients. The footgun: ignoring `poll_ready` bypasses backpressure.

WHY IT EXISTS To create a single, unified abstraction for all asynchronous request-response operations in a network application. This avoids writing separate logic for clients, servers, and middleware components, promoting code reuse and composability for concerns like logging, timeouts, or rate limiting.

THE MENTAL MODEL Think of a Service as a single, asynchronous function: async fn(Request) -> Result<Response, Error>. It's a black box that takes one thing and will eventually give you another thing back, or an error. This simple contract is powerful enough to represent an entire HTTP server, a database client connection, or a single piece of middleware like a rate limiter.

HOW IT WORKS The Service trait has two key methods: poll_ready and call. First, you call poll_ready to ask, "Can you handle a request right now?". This is the backpressure mechanism. If it returns Poll::Ready(Ok(())), the service is ready. Only then should you invoke call(request). call consumes the request and immediately returns a Future. You then .await this future to get the final Response or Error. The key rule is you cannot call call again until the future from poll_ready resolves.

WHEN TO USE IT Use Tower and its Service trait when building networked applications in Rust that require modularity and robustness. It's the foundation of popular frameworks like Axum (for web servers) and is used in clients for databases like Redis. It's ideal when you need to compose layers of functionality, such as adding logging, metrics, and timeouts to a core business logic service.

WHEN NOT TO USE IT For very simple, one-off network clients or servers where you don't foresee needing layers of middleware, the overhead of the Service trait and its ecosystem might be unnecessary. A direct tokio::net implementation could be simpler if you don't need composability. It's also not intended for synchronous, blocking operations.

ONE CANONICAL EXAMPLE An HTTP server is a Service<Request>. A timeout layer is also a Service that wraps another Service. When a request arrives, it first hits the timeout Service. The timeout service calls the inner HTTP Service, but wraps the returned Future in a race with a timer. If the timer wins, the timeout service returns an error. If the inner service's future wins, it passes the response through. This demonstrates how services compose.

Read the original → docs.rs

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.