Skip to Content
Course content

171: Middleware Layers in Tower

Click on the "Edit" button in the top corner of the screen to edit your slide content.

What's the actual difference between a Service and a Layer?

If you're coming from a framework like Express or ASP.NET, "middleware" is usually just a function you plug into a pipeline. In Tower, it's split into two distinct concepts: the Service and the Layer. This trips people up constantly.

Think of a Service as the actual worker. It's the thing that takes a request and returns a response. A Layer, on the other hand, is a factory. Its only job is to take one Service and wrap it in another Service that adds some behavior. It's like a Russian nesting doll. The Layer is the process of putting a larger doll over a smaller one; the resulting Service is the combined doll.

I usually explain it this way: the Layer is where you configure your middleware (like setting a timeout duration), and the Service is where the logic actually runs for every single request.

How do I actually implement a custom Layer without getting lost in the generics?

The trait signatures for Layer can look like a alphabet soup of generics. The trick is to define a separate Service struct to handle the logic, and then make your Layer simply instantiate that struct. Let's build a RequestTimerLayer that logs how long a request took to process.

use std::time::Instant;
use std::task::{Context, Poll};
use tower::{Layer, Service};
use futures_util::future::{BoxFuture, FutureExt};

// 1. The Service that does the actual wrapping logic
#[derive(Clone)]
pub struct TimingService<S> {
    inner: S,
}

impl<S, Request> Service<Request> for TimingService<S> 
where 
    S: Service<Request> + Send + 'static,
    S::Future: Send + 'static,
    S::Error: Send + 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: Request) -> Self::Future {
        let start = Instant::now();
        let fut = self.inner.call(req);

        async move {
            let res = fut.await;
            println!("Request took: {:?}", start.elapsed());
            res
        }.boxed()
    }
}

// 2. The Layer that produces the Service
pub struct TimingLayer;

impl<S> Layer<S> for TimingLayer {
    type Service = TimingService<S> ;

    fn layer(&self, inner: S) -> Self::Service {
        TimingService { inner }
    }
}

Notice how I used BoxFuture and .boxed(). In a real-world project, you'll find that trying to write out the exact opaque Future type for a middleware wrapper is a nightmare. Boxing the future is the standard "sanity" move here.

Why do I have to deal with poll_ready? Can't I just call the service?

This is the most common complaint I hear about Tower. It feels like unnecessary boilerplate. But poll_ready is the secret sauce that makes Tower production-ready: it's how the library implements backpressure.

In a naive system, if your database is overwhelmed, your middleware just keeps shoving requests into the queue until the app crashes with an Out-of-Memory error. In Tower, a service can return Poll::Pending in poll_ready. This tells the caller, "I'm too busy right now; don't even try to send the request yet."

When you're writing your own middleware, your primary responsibility in poll_ready is to delegate. If your layer doesn't have its own capacity limits, just call self.inner.poll_ready(cx). You're essentially asking the rest of the stack, "Is everyone below me ready to handle this?" only then do you allow the request to proceed to call().




📋 Practical Task

Implement a Request-ID Injection Layer

Your task is to create a Tower middleware layer that simulates adding a unique Request ID to every request. Since we aren't using a specific HTTP library for this exercise, assume the Request type is a String and the Response type is also a String.

  • Create a RequestIdLayer and a corresponding RequestIdService.
  • The RequestIdService should modify the incoming Request (String) by prepending a fake ID (e.g., "REQ-123: ") before passing it to the inner service.
  • Ensure poll_ready correctly delegates to the inner service.
  • Use BoxFuture for the return type of the call method to simplify the async implementation.

Test your implementation by wrapping a simple service that just returns the request string it received. If successful, the final response should contain your prepended ID.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.