Trait rocket::Sentinel

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

Required Methods§

source

fn abort(rocket: &Rocket<Ignite>) -> bool

Returns true if launch should be aborted and false otherwise.

Implementations on Foreign Types§

source§

impl<T: Sentinel> Sentinel for Option<T>

source§

fn abort(rocket: &Rocket<Ignite>) -> bool

source§

impl<T: Sentinel, E: Sentinel> Sentinel for Result<T, E>

source§

fn abort(rocket: &Rocket<Ignite>) -> bool

source§

impl<T: Sentinel, E: Sentinel> Sentinel for Either<T, E>

source§

fn abort(rocket: &Rocket<Ignite>) -> bool

Implementors§

source§

impl<T> Sentinel for Debug<T>

A sentinel that never aborts. The Responder impl for Debug will never be called, so it’s okay to not abort for failing T: Sentinel.

source§

impl<T: Send + Sync + 'static> Sentinel for &State<T>