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
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:
pre1
is invoked and executed to completion.pre2
is invoked and executed to completion.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
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:
handler
is invoked and executed to completion.post1
is invoked and executed to completion.post2
is invoked and executed to completion.
Wrapping
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:
wrap1
is invoked.next.await
is called insidewrap1
wrap2
is invoked.next.await
is called insidewrap2
handler
is invoked and executed to completion.
wrap2
completes.
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-, no early return
Let's start with a scenario where pre-processing and post-processing middlewares are present in the same request processing pipeline.
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:
pre1
is invoked and executed to completion.pre2
is invoked and executed to completion.handler
is invoked and executed to completion.post1
is invoked and executed to completion.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.
Pre- and post-, early return
Let's consider the same scenario we had above:
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
}
This time we'll assume that pre1
returns Processing::EarlyReturn
instead of Processing::Continue
.
The following sequence of events will occur:
pre1
is invoked and returnsProcessing::EarlyReturn
.pre2
is skipped.handler
is skipped.post1
is invoked and executed to completion.post2
is invoked and executed to completion.
Post-processing middlewares are still invoked, even if the request processing pipeline is interrupted by an early return.
Pre- and wrapping
Let's now consider a scenario where pre-processing and wrapping middlewares are present in the same request processing pipeline.
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:
pre1
is invoked and executed to completion.wrap1
is invoked.next.await
is called insidewrap1
pre2
is invoked and executed to completion.wrap2
is invoked.next.await
is called insidewrap3
pre3
is invoked and executed to completion.handler
is invoked and executed to completion.
wrap2
completes.
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.
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:
wrap1
is invoked.next.await
is called insidewrap1
handler
is invoked and executed to completion.post2
is invoked and executed to completion.
wrap1
completes.
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.
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:
pre1
is invoked and executed to completion.wrap1
is invoked.next.await
is called insidewrap1
pre2
is invoked and executed to completion.handler
is invoked and executed to completion.post2
is invoked and executed to completion.
wrap1
completes.
post1
is invoked and executed to completion.
Pre-, post-, and wrapping, early return
Let's consider the same scenario we had above:
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
}
This time, we'll assume that pre1
returns Processing::EarlyReturn
.
The following sequence of events will occur:
pre1
is invoked and returnsProcessing::EarlyReturn
.wrap1
is skipped.next.await
is not called insidewrap1
pre2
is skipped.handler
is skipped.post2
is skipped.
post1
is invoked and executed to completion.
Pay attention to the fact that post2
is not executed, even though it is a post-processing middleware.
That's because of wrap1
: since post2
is part of the request processing pipeline that wrap1
wraps around,
it will be skipped if wrap1
is skipped.