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-
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.
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.
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.