Skip to content

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.
Because of these limitations, Pavex provides a third type of middleware: wrapping middlewares.

src/core/mw.rs
use pavex::middleware::Next;
use pavex::response::Response;
use std::future::IntoFuture;

pub async fn middleware<C>(next: Next<C>) -> Response
where
    C: IntoFuture<Output = Response>,
{
    println!("Before the handler");
    let response = next.await;
    println!("After the handler");
    response
}

It's the most powerful kind (although they have their downsides).
They let you execute logic before and after the rest of the request processing pipeline. But, most importantly, they give you access to a future representing the rest of the request processing pipeline (the Next type), a prerequisite for those more advanced use cases.

Registration

You register a wrapping middleware against a blueprint via the wrap method.

src/core/blueprint.rs
use pavex::blueprint::router::GET;
use pavex::blueprint::Blueprint;
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.wrap(f!(super::middleware));
    bp.route(GET, "/", f!(super::handler));
    bp
}

You must provide an unambiguous path to the middleware, wrapped in the f! macro.

The middleware will be invoked for all request handlers registered after it, as long as they were registered against the same Blueprint or one of its nested children. Check out the scoping section for more details.

Registration syntax

You can use free functions, static methods, non-static methods, and trait methods as middlewares. Check out the dependency injection cookbook for more details on the syntax for each case.

IntoResponse

Wrapping middlewares, like request handlers, must return a type that can be converted into a Response via the IntoResponse trait.
If you want to return a custom type from your middleware, you must implement IntoResponse for it.

Middlewares can fail

Wrapping middlewares can be fallible, i.e. they can return a Result.

src/fallible/mw.rs
use pavex::middleware::Next;
use pavex::response::Response;
use std::future::IntoFuture;
use tokio::time::error::Elapsed;

pub async fn timeout<C>(next: Next<C>) -> Result<Response, Elapsed>
where
    C: IntoFuture<Output = Response>,
{
    let max_duration = std::time::Duration::from_secs(20);
    tokio::time::timeout(max_duration, next.into_future()).await
}

If they do, you must specify an error handler when registering them:

src/fallible/blueprint.rs
use pavex::blueprint::router::GET;
use pavex::blueprint::Blueprint;
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.wrap(f!(super::timeout))
        .error_handler(f!(super::timeout_error_handler));
        // [...]
    bp
}

Check out the error handling guide for more details.

Next

Wrapping middlewares wrap around the rest of the request processing pipeline. They are invoked before the request 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 call .await on the Next instance.

src/core/mw.rs
use pavex::middleware::Next;
use pavex::response::Response;
use std::future::IntoFuture;

pub async fn middleware<C>(next: Next<C>) -> Response
where
    C: IntoFuture<Output = Response>,
{
    println!("Before the handler");
    let response = next.await;
    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).

src/logging/mw.rs
use pavex::middleware::Next;
use pavex::response::Response;
use std::future::IntoFuture;
use tracing::Instrument;

pub async fn middleware<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 MyType as an input parameter, you'll get an error: the immutable reference to MyType borrowed by the wrapping middleware is still alive when the request handler is executed.
  • If the request handler takes MyType by value, Pavex is forced to clone the value to satisfy the borrow checker. That's inefficient. If MyType isn't clonable, you'll get an error.
  • If the request handler takes &MyType as an input parameter, all is well. You can have as many immutable references to MyType as 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.