Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
171: Middleware Layers in Tower
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
RequestIdLayerand a correspondingRequestIdService. - The
RequestIdServiceshould modify the incomingRequest(String) by prepending a fake ID (e.g.,"REQ-123: ") before passing it to the inner service. - Ensure
poll_readycorrectly delegates to the inner service. - Use
BoxFuturefor the return type of thecallmethod 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.
There are no comments for now.