Wrapping
Pre-processing and post-processing middlewares can take you a long way, but they can't do everything.
It is impossible, for example, to enforce a request-wide timeout or attach a tracing span to the request processing pipeline
using only pre-processing and post-processing middlewares.
To overcome these limitations, Pavex provides wrapping middlewares.
Wrapping middlewares can execute logic before and after the rest of the request processing pipeline.
But, most importantly, they give you access to Next, a Future representing the rest of the request processing pipeline—a prerequisite for advanced use cases.
Defining pre-processing middlewares
Use the #[wrap] attribute to define a new wrapping middleware:
use pavex::Response;
use pavex::middleware::Next;
use pavex::wrap;
use tokio::time::error::Elapsed;
#[wrap]
pub async fn timeout<C>(next: Next<C>) -> Result<Response, Elapsed>
where
C: IntoFuture<Output = Response>, // (1)!
{
let max_duration = std::time::Duration::from_secs(20);
tokio::time::timeout(max_duration, next.into_future()).await
}
- This trait bound is always required when working with
Next.
Signature
Pavex accepts a wide range of function signatures for wrapping middlewares, as long as they satisfy these requirements:
Nextmust be one of their input parameters.- Their return type must be one of the following:
- A type that implements the
IntoResponsetrait, or Result<T, E>, whereTimplementsIntoResponse.
Other than that, you have a lot of freedom in how you define your wrapping middlewares:
- They can be free functions or methods.
- They can take additional input parameters, leaning on Pavex's dependency injection system.
Registration
Invoke Blueprint::wrap to register a wrapping middleware:
use crate::timeout::TIMEOUT;
use pavex::Blueprint;
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.wrap(TIMEOUT); // (1)!
// [...]
}
TIMEOUTis a strongly-typed constant generated by the#[wrap]attribute on thetimeoutfunction.
Check out the documentation on component ids for more details.
The middleware will be invoked for all request handlers registered after it. Check out the scoping section for more details.
Middlewares can fail
Wrapping middlewares can return a Result, as shown by timeout in the example for the top-level section.
Check out the error handling guide for more details on how to handle the error case.
Next
Wrapping middlewares wrap around the rest of the request processing pipeline.
They are invoked before the route handler and all the other middlewares that were registered later.
The remaining request processing pipeline is represented by the Next type.
All middlewares must take an instance of Next as input.
To invoke the rest of the request processing pipeline, you .await the Next instance.
use pavex::Response;
use pavex::middleware::Next;
use pavex::wrap;
#[wrap]
pub async fn debug_wrapper<C>(next: Next<C>) -> Response
where
C: IntoFuture<Output = Response>,
{
println!("Before the handler");
let response = next.await; // (1)!
println!("After the handler");
response
}
You can also choose to go through the intermediate step of converting Next into a Future via the
IntoFuture trait.
This can be useful when you need to invoke APIs that wrap around a Future (e.g. tokio::time::timeout
for timeouts or tracing's .instrument() for logging).
use pavex::Response;
use pavex::middleware::Next;
use pavex::wrap;
use tracing::Instrument;
#[wrap]
pub async fn logger<C>(next: Next<C>) -> Response
where
C: IntoFuture<Output = Response>,
{
let span = tracing::info_span!("Request processing");
next.into_future().instrument(span).await
}
Dependency injection
Middlewares can take advantage of dependency injection.
You must specify the dependencies of your middleware as input parameters in its function signature.
Those inputs are going to be built and injected by the framework, according to the constructors you have registered.
Check out the dependency injection guide for more details
on how the process works.
Check out the request data guide for an overview of the data you can extract from the request
using Pavex's first-party extractors.
Use with caution
You should only use wrapping middlewares when you need to access the future representing the rest of the request processing pipeline. In all other cases, you should prefer pre-processing and post-processing middlewares.
"But why? Wrapping middlewares can do everything, why not use them all the time?"
Good question! It's because wrapping middlewares and Rust's borrow checker are an explosive combination.
Every time you inject a reference as an input parameter to a wrapping middleware, you are borrowing that reference for the whole duration of the downstream request processing pipeline. This can easily lead to borrow checker errors, especially if you are working with request-scoped dependencies. Let's unpack what that means with an example.
Example
Consider this scenario: you registered a constructor for MyType, a request-scoped dependency.
You also registered a wrapping middleware that takes &MyType as an input parameter.
You now want to work with MyType in your request handler:
- If the request handler takes
&mut MyTypeas an input parameter, you'll get an error: the immutable reference toMyTypeborrowed by the wrapping middleware is still alive when the request handler is executed. - If the request handler takes
MyTypeby value, Pavex is forced to clone the value to satisfy the borrow checker. That's inefficient. IfMyTypeisn't cloneable, you'll get an error. - If the request handler takes
&MyTypeas an input parameter, all is well. You can have as many immutable references toMyTypeas you want.
You wouldn't have these problems with pre-processing or post-processing middlewares: whatever you inject into them is going to be borrowed only while the middleware is executed. You are then free to work with those types in your request handlers/other middlewares as you please.
No &mut references
The scenario we explored above is why Pavex doesn't let you mutate request-scoped types in wrapping middlewares,
a restriction that doesn't apply to request handlers, pre-processing and post-processing middlewares.
It's so easy to shoot yourself in the foot that it's better to avoid &mut references altogether in wrapping middlewares.