When a middleware needs to modify the behavior of a response (e.g., adding a timeout), you cannot simply return the inner service's future. Instead, you must implement a custom Future that wraps the inner future and any other auxiliary futures (like a timer).
To avoid the overhead of Box<dyn Future>, implement the Future trait manually on a custom struct. Because polling a field of a pinned struct requires 'pin projection', it is highly recommended to use the pin-project crate. This allows you to safely obtain Pin<&mut Field> from a Pin<&mut Struct>.
use pin_project::pin_project;
use std::{pin::Pin, future::Future, task::{Context, Poll}};
use tokio::time::Sleep;
#[pin_project]
pub struct ResponseFuture<F> {
#[pin]
response_future: F,
#[pin]
sleep: Sleep,
}
impl<F, Response, Error> Future for ResponseFuture<F>
where
F: Future<Output = Result<Response, Error>>,
{
type Output = Result<Response, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
// 1. Poll the inner response future
match this.response_future.poll(cx) {
Poll::Ready(result) => return Poll::Ready(result),
Poll::Pending => {}
}
// 2. Poll the auxiliary future (e.g., a timer)
match this.sleep.poll(cx) {
Poll::Ready(()) => {
// Handle timeout/auxiliary completion
todo!("Return error")
}
Poll::Pending => {}
}
// 3. Neither is ready
Poll::Pending
}
}