pub trait Sentinel {
// Required method
fn abort(rocket: &Rocket<Ignite>) -> bool;
}
Expand description
An automatic last line of defense against launching an invalid Rocket
.
A sentinel, automatically run on ignition
, can trigger
a launch abort should an instance fail to meet arbitrary conditions. Every
type that appears in a mounted route’s type signature is eligible to be
a sentinel. Of these, those that implement Sentinel
have their
abort()
method invoked automatically, immediately
after ignition, once for each unique type. Sentinels inspect the finalized
instance of Rocket
and can trigger a launch abort by returning true
.
Built-In Sentinels
The State<T>
type is a sentinel that triggers an abort if the finalized
Rocket
instance is not managing state for type T
. Doing so prevents
run-time failures of the State
request guard.
Example
As an example, consider the following simple application:
#[get("/<id>")]
fn index(id: usize, state: &State<String>) -> Response {
/* ... */
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![index])
}
At ignition time, effected by the #[launch]
attribute here, Rocket probes
all types in all mounted routes for Sentinel
implementations. In this
example, the types are: usize
, State<String>
, and Response
. Those that
implement Sentinel
are queried for an abort trigger via their
Sentinel::abort()
method. In this example, the sentinel types are
State
and potentially Response
, if it implements
Sentinel
. If abort()
returns true, launch is aborted with a
corresponding error.
In this example, launch will be aborted because state of type String
is
not being managed. To correct the error and allow launching to proceed
nominally, a value of type String
must be managed:
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![index])
.manage(String::from("my managed string"))
}
Embedded Sentinels
Embedded types – type parameters of already eligible types – are also eligible to be sentinels. Consider the following route:
#[get("/")]
fn f(guard: Option<&State<String>>) -> Either<Foo, Inner<Bar>> {
unimplemented!()
}
The directly eligible sentinel types, guard and responders, are:
Option<&State<String>>
Either<Foo, Inner<Bar>>
In addition, all embedded types are also eligible. These are:
&State<String>
State<String>
String
Foo
Inner<Bar>
Bar
A type, whether embedded or not, is queried if it is a Sentinel
and none
of its parent types are sentinels. Said a different way, if every directly
eligible type is viewed as the root of an acyclic graph with edges between a
type and its type parameters, the first Sentinel
in breadth-first order
is queried:
1. Option<&State<String>> Either<Foo, Inner<Bar>>
| / \
2. &State<String> Foo Inner<Bar>
| |
3. State<String> Bar
|
4. String
In each graph above, types are queried from top to bottom, level 1 to 4.
Querying continues down paths where the parents were not sentinels. For
example, if Option
is a sentinel but Either
is not, then querying stops
for the left subgraph (Option
) but continues for the right subgraph
Either
.
Limitations
Because Rocket must know which Sentinel
implementation to query based on
its written type, generally only explicitly written, resolved, concrete
types are eligible to be sentinels. A typical application will only work
with such types, but there are several common cases to be aware of.
impl Trait
Occasionally an existential impl Trait
may find its way into return types:
use rocket::response::Responder;
#[get("/")]
fn f<'r>() -> Either<impl Responder<'r, 'static>, AnotherSentinel> {
/* ... */
}
Note: Rocket actively discourages using impl Trait
in route
signatures. In addition to impeding sentinel discovery, doing so decreases
the ability to gleam a handler’s functionality based on its type signature.
The return type of the route f
depends on its implementation. At present,
it is not possible to name the underlying concrete type of an impl Trait
at compile-time and thus not possible to determine if it implements
Sentinel
. As such, existentials are not eligible to be sentinels.
That being said, this limitation only applies per embedding: types
embedded inside of an impl Trait
are eligible. As such, in the example
above, the named AnotherSentinel
type continues to be eligible.
When possible, prefer to name all types:
#[get("/")]
fn f() -> Either<AbortingSentinel, AnotherSentinel> {
/* ... */
}
Aliases
Embedded sentinels made opaque by a type alias will fail to be considered;
the aliased type itself is considered. In the example below, only
Result<Foo, Bar>
will be considered, while the embedded Foo
and Bar
will not.
type SomeAlias = Result<Foo, Bar>;
#[get("/")]
fn f() -> SomeAlias {
/* ... */
}
Note, however, that Option<T>
and Debug<T>
are
a sentinels if T: Sentinel
, and Result<T, E>
and Either<T, E>
are
sentinels if both T: Sentinel, E: Sentinel
. Thus, for these specific
cases, a type alias will “consider” embeddings. Nevertheless, prefer to
write concrete types when possible.
Type Macros
It is impossible to determine, a priori, what a type macro will expand to. As such, Rocket is unable to determine which sentinels, if any, a type macro references, and thus no sentinels are discovered from type macros.
Even approximations are impossible. For example, consider the following:
macro_rules! MyType {
(State<'_, u32>) => (&'_ rocket::Config)
}
#[get("/")]
fn f(guard: MyType![State<'_, u32>]) {
/* ... */
}
While the MyType![State<'_, u32>]
type appears to contain a State
sentinel, the macro actually expands to &'_ rocket::Config
, which is not
the State
sentinel.
Because Rocket knows the exact syntax expected by type macros that it exports, such as the typed stream macros, discovery in these macros works as expected. You should prefer not to use type macros aside from those exported by Rocket, or if necessary, restrict your use to those that always expand to types without sentinels.
Custom Sentinels
Any type can implement Sentinel
, and the implementation can arbitrarily
inspect an ignited instance of Rocket
. For illustration, consider the
following implementation of Sentinel
for a custom Responder
which
requires:
- state for a type
T
to be managed - a catcher for status code
400
at base/
use rocket::{Rocket, Ignite, Sentinel};
impl Sentinel for MyResponder {
fn abort(rocket: &Rocket<Ignite>) -> bool {
if rocket.state::<T>().is_none() {
return true;
}
if !rocket.catchers().any(|c| c.code == Some(400) && c.base() == "/") {
return true;
}
false
}
}
If a MyResponder
is returned by any mounted route, its abort()
method
will be invoked. If the required conditions aren’t met, signaled by
returning true
from abort()
, Rocket aborts launch.