Skip to content

Execution order

Pavex provides three types of middlewares: pre-processing, post-processing, and wrapping middlewares.
When all three types of middlewares are present in the same request processing pipeline, it can be challenging to figure out the order in which they will be executed.
This guide will help you build a mental model for Pavex's runtime behaviour.

Same kind

Let's start with the simplest case: all registered middlewares are of the same kind. The middlewares will be executed in the order they were registered.
But let's review some concrete examples to make sure we're on the same page.

Pre-processing

src/pre_only/blueprint.rs
use pavex::blueprint::{Blueprint, router::GET};
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();

    bp.pre_process(f!(crate::pre1));
    bp.pre_process(f!(crate::pre2));
    bp.route(GET, "/", f!(super::handler));

    bp
}

When a request arrives, the following sequence of events will occur:

  1. pre1 is invoked and executed to completion.
  2. pre2 is invoked and executed to completion.
  3. handler is invoked and executed to completion.

If pre1 returns an early response, the rest of the request processing pipeline will be skipped—i.e. pre2 and handler will not be executed.

Post-processing

src/post_only/blueprint.rs
use pavex::blueprint::{Blueprint, router::GET};
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();

    bp.post_process(f!(crate::post1));
    bp.post_process(f!(crate::post2));
    bp.route(GET, "/", f!(super::handler));

    bp
}

When a request arrives, the following sequence of events will occur:

  1. handler is invoked and executed to completion.
  2. post1 is invoked and executed to completion.
  3. post2 is invoked and executed to completion.

Wrapping

src/wrap_only/blueprint.rs
use pavex::blueprint::{Blueprint, router::GET};
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();

    bp.wrap(f!(crate::wrap1));
    bp.wrap(f!(crate::wrap2));
    bp.route(GET, "/", f!(super::handler));

    bp
}

When a request arrives, the following sequence of events will occur:

  1. wrap1 is invoked.
    1. next.await is called inside wrap1
      1. wrap2 is invoked.
        1. next.await is called inside wrap2
          1. handler is invoked and executed to completion.
        2. wrap2 completes.
    2. wrap1 completes.

Different kinds

Let's now consider more complex scenarios: we have multiple kinds of middlewares in the same request processing pipeline.

Pre- and post-

Let's start with a scenario where pre-processing and post-processing middlewares are present in the same request processing pipeline.

src/pre_and_post/blueprint.rs
use pavex::blueprint::{Blueprint, router::GET};
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();

    bp.pre_process(f!(crate::pre1));
    bp.post_process(f!(crate::post1));
    bp.post_process(f!(crate::post2));
    bp.pre_process(f!(crate::pre2));
    bp.route(GET, "/", f!(super::handler));

    bp
}

When a request arrives, the following sequence of events will occur:

  1. pre1 is invoked and executed to completion.
  2. pre2 is invoked and executed to completion.
  3. handler is invoked and executed to completion.
  4. post1 is invoked and executed to completion.
  5. post2 is invoked and executed to completion.

Pavex doesn't care about the fact that post1 was registered before pre1.
Pre-processing middlewares are guaranteed to be executed before the request handler, and post-processing middlewares are guaranteed to be executed after the request handler. As a consequence, pre-processing middlewares will always be executed before post-processing middlewares.

Pavex relies on registration order as a way to sort middlewares of the same kind.

If pre1 returns an early response, the rest of the request processing pipeline will be skipped—i.e. pre2, handler, post1, and post2 will not be executed.

Pre- and wrapping

Let's now consider a scenario where pre-processing and wrapping middlewares are present in the same request processing pipeline.

src/pre_and_wrap/blueprint.rs
use pavex::blueprint::{Blueprint, router::GET};
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();

    bp.pre_process(f!(crate::pre1));
    bp.wrap(f!(crate::wrap1));
    bp.pre_process(f!(crate::pre2));
    bp.wrap(f!(crate::wrap2));
    bp.pre_process(f!(crate::pre3));
    bp.route(GET, "/", f!(super::handler));

    bp
}

When a request arrives, the following sequence of events will occur:

  1. pre1 is invoked and executed to completion.
  2. wrap1 is invoked.
    1. next.await is called inside wrap1
      1. pre2 is invoked and executed to completion.
      2. wrap2 is invoked.
        1. next.await is called inside wrap3
          1. pre3 is invoked and executed to completion.
          2. handler is invoked and executed to completion.
        2. wrap2 completes.
    2. wrap1 completes.

Pre-processing and wrapping middlewares can be interleaved, therefore their execution order matches the order in which they were registered.

If pre2 returns an early response, the rest of the request processing pipeline will be skipped—i.e. wrap2, pre3 and handler will not be executed. wrap1 will execute to completion, since it was already executing when pre2 was invoked. In particular, next.await in wrap1 will return the early response chosen by pre2.

Post- and wrapping

Let's now consider a scenario where post-processing and wrapping middlewares are present in the same request processing pipeline.

src/post_and_wrap/blueprint.rs
use pavex::blueprint::{Blueprint, router::GET};
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();

    bp.post_process(f!(crate::post1));
    bp.wrap(f!(crate::wrap1));
    bp.post_process(f!(crate::post2));
    bp.route(GET, "/", f!(super::handler));

    bp
}

When a request arrives, the following sequence of events will occur:

  1. wrap1 is invoked.
    1. next.await is called inside wrap1
      1. handler is invoked and executed to completion.
      2. post2 is invoked and executed to completion.
    2. wrap1 completes.
  2. post1 is invoked and executed to completion.

Wrapping middlewares must begin their execution before the request handler, therefore they will always be executed before post-processing middlewares.
Registration order matters the way out, though: wrap1 was registered before post2, therefore post2 will be part of the request processing pipeline that wrap1 wraps around, i.e. it will be invoked by next.await.
post1, on the other hand, was registered before wrap1, therefore it will be invoked after wrap1 completes.

Warning

Wrapping middlewares act as a reordering boundary.
Even though post1 was registered before post2, post2 will be executed before post1 in the example above since post2 is "captured" inside wrap1's scope.

Pre-, post-, and wrapping

At last, let's examine a scenario where all three types of middlewares are present in the same request processing pipeline.

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.pre_process(f!(crate::pre1));
    bp.post_process(f!(crate::post1));
    bp.wrap(f!(crate::wrap1));
    bp.pre_process(f!(crate::pre2));
    bp.post_process(f!(crate::post2));
    bp.route(GET, "/", f!(super::handler));

    bp
}

If there are no errors or early returns, the following sequence of events will occur:

  1. pre1 is invoked and executed to completion.
  2. wrap1 is invoked.
    1. next.await is called inside wrap1
      1. pre2 is invoked and executed to completion.
      2. handler is invoked and executed to completion.
      3. post2 is invoked and executed to completion.
    2. wrap1 completes.
  3. post1 is invoked and executed to completion.