Introduce sentinels: auto-discovered launch abort.

Sentinels resolve a long-standing usability and functional correctness
issue in Rocket: starting an application with guards and/or responders
that depend on state that isn't available. The canonical example is the
'State' guard. Prior to this commit, an application with routes that
queried unmanaged state via 'State' would fail at runtime. With this
commit, the application refuses to launch with a detailed error message.

The 'Sentinel' docs explains it as:

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

The following types are now sentinels:

  * 'contrib::databases::Connection' (any '#[database]' type)
  * 'contrib::templates::Metadata'
  * 'contrib::templates::Template'
  * 'core::State'

The following are "specialized" sentinels, which allow sentinel
discovery even through type aliases:

  * 'Option<T>', 'Debug<T>' if 'T: Sentinel'
  * 'Result<T, E>', 'Either<T, E>' if 'T: Sentinel', 'E: Sentinel'

Closes #464.
This commit is contained in:
Sergio Benitez 2021-04-16 01:23:15 -07:00
parent 1872818570
commit 64e46b7107
23 changed files with 1042 additions and 48 deletions

View File

@ -118,5 +118,11 @@ pub fn database_attr(attr: TokenStream, input: TokenStream) -> Result<TokenStrea
<#conn>::from_request(__r).await.map(Self)
}
}
impl ::rocket::Sentinel for #guard_type {
fn abort(__r: &::rocket::Rocket<::rocket::Ignite>) -> bool {
<#conn>::abort(__r)
}
}
}.into())
}

View File

@ -1,7 +1,7 @@
use std::marker::PhantomData;
use std::sync::Arc;
use rocket::{Rocket, Phase};
use rocket::{Phase, Rocket, Ignite, Sentinel};
use rocket::fairing::{AdHoc, Fairing};
use rocket::request::{Request, Outcome, FromRequest};
use rocket::outcome::IntoOutcome;
@ -196,3 +196,20 @@ impl<'r, K: 'static, C: Poolable> FromRequest<'r> for Connection<K, C> {
}
}
}
impl<K: 'static, C: Poolable> Sentinel for Connection<K, C> {
fn abort(rocket: &Rocket<Ignite>) -> bool {
use rocket::yansi::Paint;
if rocket.state::<ConnectionPool<K, C>>().is_none() {
let conn = Paint::default(std::any::type_name::<K>()).bold();
let fairing = Paint::default(format!("{}::fairing()", conn)).wrap().bold();
error!("requesting `{}` DB connection without attaching `{}`.", conn, fairing);
info_!("Attach `{}` to use database connection pooling.", fairing);
info_!("See the `contrib::database` documentation for more information.");
return true;
}
false
}
}

View File

@ -1,4 +1,4 @@
use rocket::{Request, State};
use rocket::{Request, State, Rocket, Ignite, Sentinel};
use rocket::http::Status;
use rocket::request::{self, FromRequest};
@ -28,7 +28,6 @@ use crate::templates::ContextManager;
/// }
/// }
///
///
/// fn main() {
/// rocket::build()
/// .attach(Template::fairing())
@ -81,6 +80,21 @@ impl Metadata<'_> {
}
}
impl Sentinel for Metadata<'_> {
fn abort(rocket: &Rocket<Ignite>) -> bool {
if rocket.state::<ContextManager>().is_none() {
let md = rocket::yansi::Paint::default("Metadata").bold();
let fairing = rocket::yansi::Paint::default("Template::fairing()").bold();
error!("requested `{}` guard without attaching `{}`.", md, fairing);
info_!("To use or query templates, you must attach `{}`.", fairing);
info_!("See the `Template` documentation for more information.");
return true;
}
false
}
}
/// Retrieves the template metadata. If a template fairing hasn't been attached,
/// an error is printed and an empty `Err` with status `InternalServerError`
/// (`500`) is returned.

View File

@ -137,7 +137,7 @@ use std::borrow::Cow;
use std::path::PathBuf;
use std::error::Error;
use rocket::{Rocket, Orbit};
use rocket::{Rocket, Orbit, Ignite, Sentinel};
use rocket::request::Request;
use rocket::fairing::Fairing;
use rocket::response::{self, Content, Responder};
@ -433,3 +433,18 @@ impl<'r> Responder<'r, 'static> for Template {
Content(content_type, render).respond_to(req)
}
}
impl Sentinel for Template {
fn abort(rocket: &Rocket<Ignite>) -> bool {
if rocket.state::<ContextManager>().is_none() {
let template = rocket::yansi::Paint::default("Template").bold();
let fairing = rocket::yansi::Paint::default("Template::fairing()").bold();
error!("returning `{}` responder without attaching `{}`.", template, fairing);
info_!("To use or query templates, you must attach `{}`.", fairing);
info_!("See the `Template` documentation for more information.");
return true;
}
false
}
}

View File

@ -56,9 +56,9 @@ mod rusqlite_integration_test {
}
}
#[cfg(feature = "databases")]
#[cfg(test)]
mod drop_runtime_test {
#[cfg(feature = "databases")]
mod sentinel_and_runtime_test {
use rocket::{Rocket, Build};
use r2d2::{ManageConnection, Pool};
use rocket_contrib::databases::{database, Poolable, PoolResult};
@ -107,4 +107,15 @@ mod drop_runtime_test {
let rocket = rocket::custom(config).attach(TestDb::fairing());
drop(rocket);
}
#[test]
fn test_sentinel() {
use rocket::{*, local::blocking::Client, error::ErrorKind::SentinelAborts};
#[get("/")]
fn use_db(_db: TestDb) {}
let err = Client::debug_with(routes![use_db]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
}
}

View File

@ -47,6 +47,60 @@ mod templates_tests {
}
}
#[test]
fn test_sentinel() {
use rocket::{local::blocking::Client, error::ErrorKind::SentinelAborts};
let err = Client::debug_with(routes![is_reloading]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
let err = Client::debug_with(routes![is_reloading, template_check]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 2));
#[get("/")]
fn return_template() -> Template {
Template::render("foo", ())
}
let err = Client::debug_with(routes![return_template]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
#[get("/")]
fn return_opt_template() -> Option<Template> {
Some(Template::render("foo", ()))
}
let err = Client::debug_with(routes![return_opt_template]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
#[derive(rocket::Responder)]
struct MyThing<T>(T);
#[get("/")]
fn return_custom_template() -> MyThing<Template> {
MyThing(Template::render("foo", ()))
}
let err = Client::debug_with(routes![return_custom_template]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
#[derive(rocket::Responder)]
struct MyOkayThing<T>(Option<T>);
impl<T> rocket::Sentinel for MyOkayThing<T> {
fn abort(_: &Rocket<rocket::Ignite>) -> bool {
false
}
}
#[get("/")]
fn always_ok_sentinel() -> MyOkayThing<Template> {
MyOkayThing(None)
}
Client::debug_with(routes![always_ok_sentinel]).expect("no sentinel abort");
}
#[cfg(feature = "tera_templates")]
mod tera_tests {
use super::*;

View File

@ -2,16 +2,23 @@ mod parse;
use proc_macro2::{TokenStream, Span};
use devise::{Spanned, SpanWrapped, Result, FromMeta, Diagnostic};
use devise::ext::TypeExt as _;
use crate::{proc_macro2, syn};
use crate::proc_macro_ext::StringLit;
use crate::syn_ext::IdentExt;
use crate::syn_ext::{IdentExt, TypeExt as _};
use crate::http_codegen::{Method, Optional};
use crate::attribute::param::Guard;
use parse::{Route, Attribute, MethodAttribute};
impl Route {
pub fn guards(&self) -> impl Iterator<Item = &Guard> {
self.param_guards()
.chain(self.query_guards())
.chain(self.request_guards.iter())
}
pub fn param_guards(&self) -> impl Iterator<Item = &Guard> {
self.path_params.iter().filter_map(|p| p.guard())
}
@ -229,6 +236,56 @@ fn responder_outcome_expr(route: &Route) -> TokenStream {
}
}
fn sentinels_expr(route: &Route) -> TokenStream {
let ret_ty = match route.handler.sig.output {
syn::ReturnType::Default => None,
syn::ReturnType::Type(_, ref ty) => Some(ty.with_stripped_lifetimes())
};
let generic_idents: Vec<_> = route.handler.sig.generics
.type_params()
.map(|p| &p.ident)
.collect();
// Note: for a given route, we need to emit a valid graph of eligble
// sentinels. This means that we don't have broken links, where a child
// points to a parent that doesn't exist. The concern is that the
// `is_concrete()` filter will cause a break in the graph.
//
// Here's a proof by cases for why this can't happen:
// 1. if `is_concrete()` returns `false` for a (valid) type, it returns
// false for all of its parents. we consider this an axiom; this is
// the point of `is_concrete()`. the type is filtered out, so the
// theorem vacously holds
// 2. if `is_concrete()` returns `true`, for a type `T`, it either:
// * returns `false` for the parent. by 1) it will return false for
// _all_ parents of the type, so no node in the graph can consider,
// directly or indirectly, `T` to be a child, and thus there are no
// broken links; the thereom holds
// * returns `true` for the parent, and so the type has a parent, and
// the theorem holds.
// 3. these are all the cases. QED.
let eligible_types = route.guards()
.map(|guard| &guard.ty)
.chain(ret_ty.as_ref().into_iter())
.flat_map(|ty| ty.unfold())
.filter(|ty| ty.is_concrete(&generic_idents))
.map(|child| (child.parent, child.ty));
let sentinel = eligible_types.map(|(parent, ty)| {
define_spanned_export!(ty.span() => _sentinel);
match parent {
Some(p) if p.is_concrete(&generic_idents) => {
quote_spanned!(ty.span() => #_sentinel::resolve!(#ty, #p))
}
Some(_) | None => quote_spanned!(ty.span() => #_sentinel::resolve!(#ty)),
}
});
quote!(::std::vec![#(#sentinel),*])
}
fn codegen_route(route: Route) -> Result<TokenStream> {
use crate::exports::*;
@ -238,6 +295,9 @@ fn codegen_route(route: Route) -> Result<TokenStream> {
let query_guards = query_decls(&route);
let data_guard = route.data_guard.as_ref().map(data_guard_decl);
// Extract the sentinels from the route.
let sentinels = sentinels_expr(&route);
// Gather info about the function.
let (vis, handler_fn) = (&route.handler.vis, &route.handler);
let handler_fn_name = &handler_fn.sig.ident;
@ -245,7 +305,7 @@ fn codegen_route(route: Route) -> Result<TokenStream> {
let responder_outcome = responder_outcome_expr(&route);
let method = route.attr.method;
let path = route.attr.uri.to_string();
let uri = route.attr.uri.to_string();
let rank = Optional(route.attr.rank);
let format = Optional(route.attr.format.as_ref());
@ -278,10 +338,11 @@ fn codegen_route(route: Route) -> Result<TokenStream> {
#_route::StaticInfo {
name: stringify!(#handler_fn_name),
method: #method,
path: #path,
uri: #uri,
handler: monomorphized_function,
format: #format,
rank: #rank,
sentinels: #sentinels,
}
}
}

View File

@ -145,15 +145,14 @@ impl Route {
let value = (ident.clone(), ty.with_stripped_lifetimes());
arguments.map.insert(Name::from(ident), value);
} else {
let error = match arg.wild() {
Some(_) => "handler arguments cannot be ignored",
None => "handler arguments must be of the form `ident: Type`"
let span = arg.span();
let diag = if arg.wild().is_some() {
span.error("handler arguments must be named")
.help("to name an ignored handler argument, use `_name`")
} else {
span.error("handler arguments must be of the form `ident: Type`")
};
let diag = arg.span()
.error(error)
.note("handler arguments must be of the form: `ident: Type`");
diags.push(diag);
}
}

View File

@ -71,6 +71,7 @@ define_exported_paths! {
_response => ::rocket::response,
_route => ::rocket::route,
_catcher => ::rocket::catcher,
_sentinel => ::rocket::sentinel,
_log => ::rocket::logger,
_form => ::rocket::form::prelude,
_http => ::rocket::http,

View File

@ -1,6 +1,8 @@
//! Extensions to `syn` types.
use crate::syn::{self, Ident, ext::IdentExt as _};
use std::ops::Deref;
use crate::syn::{self, Ident, ext::IdentExt as _, visit::Visit};
use crate::proc_macro2::Span;
pub trait IdentExt {
@ -23,6 +25,25 @@ pub trait FnArgExt {
fn wild(&self) -> Option<&syn::PatWild>;
}
#[derive(Debug)]
pub struct Child<'a> {
pub parent: Option<&'a syn::Type>,
pub ty: &'a syn::Type,
}
impl Deref for Child<'_> {
type Target = syn::Type;
fn deref(&self) -> &Self::Target {
&self.ty
}
}
pub trait TypeExt {
fn unfold(&self) -> Vec<Child<'_>>;
fn is_concrete(&self, generic_ident: &[&Ident]) -> bool;
}
impl IdentExt for syn::Ident {
fn prepend(&self, string: &str) -> syn::Ident {
syn::Ident::new(&format!("{}{}", string, self.unraw()), self.span())
@ -81,3 +102,72 @@ impl FnArgExt for syn::FnArg {
}
}
}
impl TypeExt for syn::Type {
fn unfold(&self) -> Vec<Child<'_>> {
#[derive(Default)]
struct Visitor<'a> {
parents: Vec<&'a syn::Type>,
children: Vec<Child<'a>>,
}
impl<'a> Visit<'a> for Visitor<'a> {
fn visit_type(&mut self, ty: &'a syn::Type) {
self.children.push(Child { parent: self.parents.last().cloned(), ty });
self.parents.push(ty);
syn::visit::visit_type(self, ty);
self.parents.pop();
}
}
let mut visitor = Visitor::default();
visitor.visit_type(self);
visitor.children
}
fn is_concrete(&self, generics: &[&Ident]) -> bool {
struct ConcreteVisitor<'i>(bool, &'i [&'i Ident]);
impl<'a, 'i> Visit<'a> for ConcreteVisitor<'i> {
fn visit_type(&mut self, ty: &'a syn::Type) {
use syn::Type::*;
match ty {
Path(t) if self.1.iter().any(|i| t.path.is_ident(*i)) => {
self.0 = false;
return;
}
ImplTrait(_) | Infer(_) => {
self.0 = false;
return;
}
BareFn(_) | Never(_) => {
self.0 = true;
return;
},
_ => syn::visit::visit_type(self, ty),
}
}
}
let mut visitor = ConcreteVisitor(true, generics);
visitor.visit_type(self);
visitor.0
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_type_unfold_is_generic() {
use super::{TypeExt, syn};
let ty: syn::Type = syn::parse_quote!(A<B, C<impl Foo>, Box<dyn Foo>, Option<T>>);
let children = ty.unfold();
assert_eq!(children.len(), 8);
let gen_ident = format_ident!("T");
let gen = &[&gen_ident];
assert_eq!(children.iter().filter(|c| c.ty.is_concrete(gen)).count(), 3);
}
}

View File

@ -68,13 +68,13 @@ error: expected key/value `key = value`
33 | #[get("/", ...)]
| ^^^
error: handler arguments cannot be ignored
error: handler arguments must be named
--> $DIR/route-attribute-general-syntax.rs:39:7
|
39 | fn c1(_: usize) {}
| ^^^^^^^^
|
= note: handler arguments must be of the form: `ident: Type`
= help: to name an ignored handler argument, use `_name`
error: invalid value: expected string literal
--> $DIR/route-attribute-general-syntax.rs:43:7

View File

@ -203,13 +203,13 @@ error: invalid identifier: `test `
= help: dynamic parameters must be valid identifiers
= help: did you mean `<test>`?
error: handler arguments cannot be ignored
error: handler arguments must be named
--> $DIR/route-path-bad-syntax.rs:89:7
|
89 | fn k0(_: usize) {}
| ^^^^^^^^
|
= note: handler arguments must be of the form: `ident: Type`
= help: to name an ignored handler argument, use `_name`
error: parameters cannot be empty
--> $DIR/route-path-bad-syntax.rs:93:9

View File

@ -64,8 +64,8 @@ error: expected key/value `key = value`
33 | #[get("/", ...)]
| ^^^
error: handler arguments cannot be ignored
--- note: handler arguments must be of the form: `ident: Type`
error: handler arguments must be named
--- help: to name an ignored handler argument, use `_name`
--> $DIR/route-attribute-general-syntax.rs:39:7
|
39 | fn c1(_: usize) {}

View File

@ -168,8 +168,8 @@ error: invalid identifier: `test `
83 | #[get("/", data = "<test >")]
| ^^^^^^^^^
error: handler arguments cannot be ignored
--- note: handler arguments must be of the form: `ident: Type`
error: handler arguments must be named
--- help: to name an ignored handler argument, use `_name`
--> $DIR/route-path-bad-syntax.rs:89:7
|
89 | fn k0(_: usize) {}

View File

@ -70,6 +70,7 @@ pub struct Error {
/// encountered an error; these are represented by the `Collision` and
/// `FailedFairing` variants, respectively.
#[derive(Debug)]
#[non_exhaustive]
pub enum ErrorKind {
/// Binding to the provided address/port failed.
Bind(io::Error),
@ -84,6 +85,8 @@ pub enum ErrorKind {
Collisions(crate::router::Collisions),
/// Launch fairing(s) failed.
FailedFairings(Vec<crate::fairing::Info>),
/// Sentinels requested abort.
SentinelAborts(Vec<crate::sentinel::Sentry>),
/// The configuration profile is not debug but not secret key is configured.
InsecureSecretKey(Profile),
}
@ -142,10 +145,11 @@ impl fmt::Display for ErrorKind {
ErrorKind::Bind(e) => write!(f, "binding failed: {}", e),
ErrorKind::Io(e) => write!(f, "I/O error: {}", e),
ErrorKind::Collisions(_) => "collisions detected".fmt(f),
ErrorKind::FailedFairings(_) => "a launch fairing failed".fmt(f),
ErrorKind::FailedFairings(_) => "launch fairing(s) failed".fmt(f),
ErrorKind::Runtime(e) => write!(f, "runtime error: {}", e),
ErrorKind::InsecureSecretKey(_) => "insecure secret key config".fmt(f),
ErrorKind::Config(_) => "failed to extract configuration".fmt(f),
ErrorKind::SentinelAborts(_) => "sentinel(s) aborted".fmt(f),
}
}
}
@ -205,7 +209,7 @@ impl Drop for Error {
info_!("{}", fairing.name);
}
panic!("aborting due to launch fairing failure");
panic!("aborting due to fairing failure(s)");
}
ErrorKind::Runtime(ref err) => {
error!("An error occured in the runtime:");
@ -214,7 +218,7 @@ impl Drop for Error {
}
ErrorKind::InsecureSecretKey(profile) => {
error!("secrets enabled in non-debug without `secret_key`");
info_!("selected profile: {}", Paint::white(profile));
info_!("selected profile: {}", Paint::default(profile).bold());
info_!("disable `secrets` feature or configure a `secret_key`");
panic!("aborting due to insecure configuration")
}
@ -222,6 +226,16 @@ impl Drop for Error {
crate::config::pretty_print_error(error.clone());
panic!("aborting due to invalid configuration")
}
ErrorKind::SentinelAborts(ref failures) => {
error!("Rocket failed to launch due to aborting sentinels:");
for sentry in failures {
let name = Paint::default(sentry.type_name).bold();
let (file, line, col) = sentry.location;
info_!("{} ({}:{}:{})", name, file, line, col);
}
panic!("aborting due to sentinel-triggered abort(s)");
}
}
}
}

View File

@ -119,6 +119,7 @@ pub use figment;
#[macro_use] pub mod logger;
#[macro_use] pub mod outcome;
#[macro_use] pub mod data;
#[doc(hidden)] pub mod sentinel;
pub mod local;
pub mod request;
pub mod response;
@ -161,6 +162,7 @@ mod phase;
#[doc(hidden)] pub use either::Either;
#[doc(inline)] pub use phase::{Phase, Build, Ignite, Orbit};
#[doc(inline)] pub use error::Error;
#[doc(inline)] pub use sentinel::Sentinel;
pub use crate::rocket::Rocket;
pub use crate::request::Request;
pub use crate::shutdown::Shutdown;

View File

@ -3,12 +3,12 @@ use std::ops::{Deref, DerefMut};
use std::convert::TryInto;
use std::sync::Arc;
use figment::{Figment, Provider};
use either::Either;
use yansi::Paint;
use either::Either;
use tokio::sync::Notify;
use figment::{Figment, Provider};
use crate::{Route, Catcher, Config, Shutdown};
use crate::{Route, Catcher, Config, Shutdown, sentinel};
use crate::router::Router;
use crate::fairing::{Fairing, Fairings};
use crate::phase::{Phase, Build, Building, Ignite, Igniting, Orbit, Orbiting};
@ -431,6 +431,7 @@ impl Rocket<Build> {
/// secret key.
/// * There are no [`Route#collisions`] or [`Catcher#collisions`]
/// collisions.
/// * No [`Sentinel`](crate::Sentinel) triggered an abort.
///
/// If any of these conditions fail to be met, a respective [`Error`] is
/// returned.
@ -502,13 +503,19 @@ impl Rocket<Build> {
self.fairings.pretty_print();
// Ignite the rocket.
Ok(Rocket(Igniting {
let rocket: Rocket<Ignite> = Rocket(Igniting {
router, config,
shutdown: Arc::new(Notify::new()),
figment: self.0.figment,
fairings: self.0.fairings,
state: self.0.state,
}))
});
// Query the sentinels, abort if requested.
let sentinels = rocket.routes().flat_map(|r| r.sentinels.iter());
sentinel::query(sentinels, &rocket).map_err(ErrorKind::SentinelAborts)?;
Ok(rocket)
}
}

View File

@ -6,6 +6,7 @@ use yansi::Paint;
use crate::http::{uri, Method, MediaType};
use crate::route::{Handler, RouteUri, BoxFuture};
use crate::sentinel::Sentry;
/// A request handling route.
///
@ -187,6 +188,8 @@ pub struct Route {
pub rank: isize,
/// The media type this route matches against, if any.
pub format: Option<MediaType>,
/// The discovered sentinels.
pub(crate) sentinels: Vec<Sentry>,
}
impl Route {
@ -247,6 +250,7 @@ impl Route {
Route {
name: None,
format: None,
sentinels: Vec::new(),
handler: Box::new(handler),
rank, uri, method,
}
@ -330,27 +334,33 @@ pub struct StaticInfo {
pub name: &'static str,
/// The route's method.
pub method: Method,
/// The route's path, without the base mount point.
pub path: &'static str,
/// The route's URi, without the base mount point.
pub uri: &'static str,
/// The route's format, if any.
pub format: Option<MediaType>,
/// The route's handler, i.e, the annotated function.
pub handler: for<'r> fn(&'r crate::Request<'_>, crate::Data) -> BoxFuture<'r>,
/// The route's rank, if any.
pub rank: Option<isize>,
/// Route-derived sentinels, if any.
/// This isn't `&'static [SentryInfo]` because `type_name()` isn't `const`.
pub sentinels: Vec<Sentry>,
}
#[doc(hidden)]
impl From<StaticInfo> for Route {
fn from(info: StaticInfo) -> Route {
// This should never panic since `info.path` is statically checked.
let mut route = Route::new(info.method, info.path, info.handler);
route.format = info.format;
route.name = Some(info.name.into());
if let Some(rank) = info.rank {
route.rank = rank;
}
let uri = RouteUri::new("/", info.uri);
route
Route {
name: Some(info.name.into()),
method: info.method,
handler: Box::new(info.handler),
rank: info.rank.unwrap_or_else(|| uri.default_rank()),
format: info.format,
sentinels: info.sentinels.into_iter().collect(),
uri,
}
}
}

View File

@ -77,8 +77,8 @@ impl Router {
}
}
fn collisions<'a, I: 'a, T: 'a>(&self, items: I) -> impl Iterator<Item = (T, T)> + 'a
where I: Iterator<Item = &'a T> + Clone, T: Collide + Clone,
fn collisions<'a, I, T>(&self, items: I) -> impl Iterator<Item = (T, T)> + 'a
where I: Iterator<Item = &'a T> + Clone + 'a, T: Collide + Clone + 'a,
{
items.clone().enumerate()
.flat_map(move |(i, a)| {

438
core/lib/src/sentinel.rs Normal file
View File

@ -0,0 +1,438 @@
use std::fmt;
use std::any::TypeId;
use crate::{Rocket, Ignite};
/// An automatic last line of defense against launching an invalid [`Rocket`].
///
/// A sentinel, automatically run on [`ignition`](Rocket::ignite()), 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()`](Sentinel::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.
///
/// [`State<T>`]: crate::State
/// [`State`]: crate::State
///
/// ## Example
///
/// As an example, consider the following simple application:
///
/// ```rust
/// # use rocket::*;
/// # type Response = ();
/// #[get("/<id>")]
/// fn index(id: usize, state: State<String>) -> Response {
/// /* ... */
/// }
///
/// #[launch]
/// fn rocket() -> _ {
/// rocket::build().mount("/", routes![index])
/// }
///
/// # use rocket::{Config, error::ErrorKind};
/// # rocket::async_test(async {
/// # let result = rocket().configure(Config::debug_default()).ignite().await;
/// # assert!(matches!(result.unwrap_err().kind(), ErrorKind::SentinelAborts(..)));
/// # })
/// ```
///
/// 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:
///
/// ```rust
/// # use rocket::*;
/// # type Response = ();
/// # #[get("/<id>")]
/// # fn index(id: usize, state: State<String>) -> Response {
/// # /* ... */
/// # }
/// #
/// #[launch]
/// fn rocket() -> _ {
/// rocket::build()
/// .mount("/", routes![index])
/// .manage(String::from("my managed string"))
/// }
///
/// # use rocket::{Config, error::ErrorKind};
/// # rocket::async_test(async {
/// # rocket().configure(Config::debug_default()).ignite().await.unwrap();
/// # })
/// ```
///
/// # Embedded Sentinels
///
/// Embedded types -- type parameters of already eligble types -- are also
/// eligible to be sentinels. Consider the following route:
///
/// ```rust
/// # use rocket::*;
/// # use either::Either;
/// # type Inner<T> = Option<T>;
/// # type Foo = ();
/// # type Bar = ();
/// #[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_ eligble. These are:
///
/// * `State<'_, 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 each graph, in
/// breadth-first order, is queried:
///
/// ```text
/// Option<State<'_, String>> Either<Foo, Inner<Bar>>
/// | / \
/// State<'_, String> Foo Inner<Bar>
/// |
/// Bar
/// ```
///
/// Neither `Option` nor `Either` are sentinels, so they won't be queried. In
/// the next level, `State` is a `Sentinel`, so it _is_ queried. If `Foo` is a
/// sentinel, it is queried as well. If `Inner` is a sentinel, it is queried,
/// and traversal stops without considering `Bar`. If `Inner` is _not_ a
/// `Sentinel`, `Bar` is considered and queried if it is a sentinel.
///
/// # Limitations
///
/// Because Rocket must know which `Sentinel` implementation to query based on
/// its _written_ type, only explicitly written, resolved, concrete types are
/// eligible to be sentinels. Most application will only work with such types,
/// but occasionally an existential `impl Trait` may find its way into return
/// types:
///
/// ```rust
/// # use rocket::*;
/// # use either::Either;
/// use rocket::response::Responder;
/// # type AnotherSentinel = ();
///
/// #[get("/")]
/// fn f<'r>() -> Either<impl Responder<'r, 'static>, AnotherSentinel> {
/// /* ... */
/// # Either::Left(())
/// }
/// ```
///
/// **Note:** _Rocket actively discourages using `impl Trait` in route
/// signatures. In addition to impeding sentinel discovery, doing so decreases
/// the ability to gleam handler 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. This
/// limitation applies per embedded type: the directly named `AnotherSentinel`
/// type continues to be eligible to be a sentinel.
///
/// When possible, prefer to name all types:
///
/// ```rust
/// # use rocket::*;
/// # use either::Either;
/// # type AbortingSentinel = ();
/// # type AnotherSentinel = ();
/// #[get("/")]
/// fn f() -> Either<AbortingSentinel, AnotherSentinel> {
/// /* ... */
/// # unimplemented!()
/// }
/// ```
///
/// ## Aliases
///
/// Embedded discovery of sentinels is syntactic in nature: an embedded sentinel
/// is only discovered if its named in the type. As such, sentinels made opaque
/// by a type alias will fail to be considered. In the example below, only
/// `Result<Foo, Bar>` will be considered, while the embedded `Foo` and `Bar`
/// will not.
///
/// ```rust
/// # use rocket::get;
/// # type Foo = ();
/// # type Bar = ();
/// type SomeAlias = Result<Foo, Bar>;
///
/// #[get("/")]
/// fn f() -> SomeAlias {
/// /* ... */
/// # unimplemented!()
/// }
/// ```
///
/// Note, however, that `Option<T>` and [`Debug<T>`](crate::response::Debug) 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.
///
/// # Custom Sentinels
///
/// Any type can implement `Sentinel`, and the implementation can arbitrarily
/// inspect the passed in 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 as well as catcher for status
/// code `400` at base `/`:
///
/// ```rust
/// use rocket::{Rocket, Ignite, Sentinel};
/// # struct MyResponder;
/// # struct T;
///
/// 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, and launch will be aborted if the method returns `true`.
pub trait Sentinel {
/// Returns `true` if launch should be aborted and `false` otherwise.
fn abort(rocket: &Rocket<Ignite>) -> bool;
}
impl<T: Sentinel> Sentinel for Option<T> {
fn abort(rocket: &Rocket<Ignite>) -> bool {
T::abort(rocket)
}
}
impl<T: Sentinel, E: Sentinel> Sentinel for Result<T, E> {
fn abort(rocket: &Rocket<Ignite>) -> bool {
// We want to run _both_, _without_ short-circuiting, for the logs.
let left = T::abort(rocket);
let right = E::abort(rocket);
left || right
}
}
impl<T: Sentinel, E: Sentinel> Sentinel for either::Either<T, E> {
fn abort(rocket: &Rocket<Ignite>) -> bool {
// We want to run _both_, _without_ short-circuiting, for the logs.
let left = T::abort(rocket);
let right = E::abort(rocket);
left || right
}
}
/// A sentinel that never aborts.
impl<T> Sentinel for crate::response::Debug<T> {
fn abort(_: &Rocket<Ignite>) -> bool {
false
}
}
/// The information resolved from a `T: ?Sentinel` by the `resolve!()` macro.
#[derive(Clone, Copy)]
pub struct Sentry {
/// The type ID of `T`.
pub type_id: TypeId,
/// The type name `T` as a string.
pub type_name: &'static str,
/// The type ID of type in which `T` is nested if not a top-level type.
pub parent: Option<TypeId>,
/// The source (file, column, line) location of the resolved `T`.
pub location: (&'static str, u32, u32),
/// The value of `<T as Sentinel>::SPECIALIZED` or the fallback.
///
/// This is `true` when `T: Sentinel` and `false` when `T: !Sentinel`.
pub specialized: bool,
/// The value of `<T as Sentinel>::abort` or the fallback.
pub abort: fn(&Rocket<Ignite>) -> bool,
}
/// Query `sentinels`, once for each unique `type_id`, returning an `Err` of all
/// of the sentinels that triggered an abort or `Ok(())` if none did.
pub(crate) fn query<'s>(
sentinels: impl Iterator<Item = &'s Sentry>,
rocket: &Rocket<Ignite>,
) -> Result<(), Vec<Sentry>> {
use std::collections::{HashMap, VecDeque};
// Build a graph of the sentinels.
let mut roots: VecDeque<&'s Sentry> = VecDeque::new();
let mut map: HashMap<TypeId, VecDeque<&'s Sentry>> = HashMap::new();
for sentinel in sentinels {
match sentinel.parent {
Some(parent) => map.entry(parent).or_default().push_back(sentinel),
None => roots.push_back(sentinel),
}
}
// Traverse the graph in breadth-first order. If we find a specialized
// sentinel, query it (once for a unique type) and don't traverse its
// children. Otherwise, traverse its children. Record queried aborts.
let mut remaining = roots;
let mut visited: HashMap<TypeId, bool> = HashMap::new();
let mut aborted = vec![];
while let Some(sentinel) = remaining.pop_front() {
if sentinel.specialized {
if *visited.entry(sentinel.type_id).or_insert_with(|| (sentinel.abort)(rocket)) {
aborted.push(sentinel);
}
} else if let Some(mut children) = map.remove(&sentinel.type_id) {
remaining.append(&mut children);
}
}
match aborted.is_empty() {
true => Ok(()),
false => Err(aborted.into_iter().cloned().collect())
}
}
impl fmt::Debug for Sentry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Sentry")
.field("type_id", &self.type_id)
.field("type_name", &self.type_name)
.field("parent", &self.parent)
.field("location", &self.location)
.field("default", &self.specialized)
.finish()
}
}
/// Resolves a `T` to the specialized or fallback implementation of
/// `Sentinel`, returning a `Sentry` struct with the resolved items.
#[doc(hidden)]
#[macro_export]
macro_rules! resolve {
($T:ty $(, $P:ty)?) => ({
#[allow(unused_imports)]
use $crate::sentinel::resolution::{Resolve, DefaultSentinel as _};
$crate::sentinel::Sentry {
type_id: std::any::TypeId::of::<$T>(),
type_name: std::any::type_name::<$T>(),
parent: None $(.or(Some(std::any::TypeId::of::<$P>())))?,
location: (std::file!(), std::line!(), std::column!()),
specialized: Resolve::<$T>::SPECIALIZED,
abort: Resolve::<$T>::abort,
}
})
}
pub use resolve;
pub mod resolution {
use super::*;
/// The *magic*.
///
/// `Resolve<T>::item` for `T: Sentinel` is `<T as Sentinel>::item`.
/// `Resolve<T>::item` for `T: !Sentinel` is `DefaultSentinel::item`.
///
/// This _must_ be used as `Resolve::<T>:item` for resolution to work. This
/// is a fun, static dispatch hack for "specialization" that works because
/// Rust prefers inherent methods over blanket trait impl methods.
pub struct Resolve<T: ?Sized>(std::marker::PhantomData<T>);
/// Fallback trait "implementing" `Sentinel` for all types. This is what
/// Rust will resolve `Resolve<T>::item` to when `T: !Sentinel`.
pub trait DefaultSentinel {
const SPECIALIZED: bool = false;
fn abort(_: &Rocket<Ignite>) -> bool { false }
}
impl<T: ?Sized> DefaultSentinel for T {}
/// "Specialized" "implementation" of `Sentinel` for `T: Sentinel`. This is
/// what Rust will resolve `Resolve<T>::item` to when `T: Sentinel`.
impl<T: Sentinel + ?Sized> Resolve<T> {
pub const SPECIALIZED: bool = true;
pub fn abort(rocket: &Rocket<Ignite>) -> bool {
T::abort(rocket)
}
}
}
#[cfg(test)]
mod test {
use std::any::TypeId;
use crate::sentinel::resolve;
struct NotASentinel;
struct YesASentinel;
impl super::Sentinel for YesASentinel {
fn abort(_: &crate::Rocket<crate::Ignite>) -> bool {
unimplemented!()
}
}
#[test]
fn check_can_determine() {
let not_a_sentinel = resolve!(NotASentinel);
assert!(not_a_sentinel.type_name.ends_with("NotASentinel"));
assert!(!not_a_sentinel.specialized);
let yes_a_sentinel = resolve!(YesASentinel);
assert!(yes_a_sentinel.type_name.ends_with("YesASentinel"));
assert!(yes_a_sentinel.specialized);
}
struct HasSentinel<T>(T);
#[test]
fn parent_works() {
let child = resolve!(YesASentinel, HasSentinel<YesASentinel>);
assert!(child.type_name.ends_with("YesASentinel"));
assert_eq!(child.parent.unwrap(), TypeId::of::<HasSentinel<YesASentinel>>());
assert!(child.specialized);
let not_a_direct_sentinel = resolve!(HasSentinel<YesASentinel>);
assert!(not_a_direct_sentinel.type_name.contains("HasSentinel"));
assert!(not_a_direct_sentinel.type_name.contains("YesASentinel"));
assert!(not_a_direct_sentinel.parent.is_none());
assert!(!not_a_direct_sentinel.specialized);
}
}

View File

@ -1,6 +1,7 @@
use std::ops::Deref;
use std::any::type_name;
use crate::{Rocket, Phase};
use crate::{Phase, Rocket, Ignite, Sentinel};
use crate::request::{self, FromRequest, Request};
use crate::outcome::Outcome;
use crate::http::Status;
@ -177,13 +178,26 @@ impl<'r, T: Send + Sync + 'static> FromRequest<'r> for State<'r, T> {
match req.rocket().state::<T>() {
Some(state) => Outcome::Success(State(state)),
None => {
error_!("Attempted to retrieve unmanaged state `{}`!", std::any::type_name::<T>());
error_!("Attempted to retrieve unmanaged state `{}`!", type_name::<T>());
Outcome::Failure((Status::InternalServerError, ()))
}
}
}
}
impl<T: Send + Sync + 'static> Sentinel for State<'_, T> {
fn abort(rocket: &Rocket<Ignite>) -> bool {
if rocket.state::<T>().is_none() {
let type_name = yansi::Paint::default(type_name::<T>()).bold();
error!("launching with unmanaged `{}` state.", type_name);
info_!("Using `State` requires managing it with `.manage()`.");
return true;
}
false
}
}
impl<T: Send + Sync + 'static> Deref for State<'_, T> {
type Target = T;

236
core/lib/tests/sentinel.rs Normal file
View File

@ -0,0 +1,236 @@
use rocket::{*, error::ErrorKind::SentinelAborts};
#[get("/two")]
fn two_states(_one: State<u32>, _two: State<String>) {}
#[get("/one")]
fn one_state(_three: State<u8>) {}
#[async_test]
async fn state_sentinel_works() {
let err = rocket::build()
.configure(Config::debug_default())
.mount("/", routes![two_states])
.ignite().await
.unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 2));
let err = rocket::build()
.configure(Config::debug_default())
.mount("/", routes![two_states])
.manage(String::new())
.ignite().await
.unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
let err = rocket::build()
.configure(Config::debug_default())
.mount("/", routes![two_states])
.manage(1 as u32)
.ignite().await
.unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
let result = rocket::build()
.configure(Config::debug_default())
.mount("/", routes![two_states])
.manage(String::new())
.manage(1 as u32)
.ignite().await;
assert!(result.is_ok());
let err = rocket::build()
.configure(Config::debug_default())
.mount("/", routes![one_state])
.ignite().await
.unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
let result = rocket::build()
.configure(Config::debug_default())
.mount("/", routes![one_state])
.manage(1 as u8)
.ignite().await;
assert!(result.is_ok());
let err = rocket::build()
.configure(Config::debug_default())
.mount("/", routes![one_state, two_states])
.ignite().await
.unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 3));
let err = rocket::build()
.configure(Config::debug_default())
.mount("/", routes![one_state, two_states])
.manage(1 as u32)
.ignite().await
.unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 2));
let err = rocket::build()
.configure(Config::debug_default())
.mount("/", routes![one_state, two_states])
.manage(1 as u8)
.ignite().await
.unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 2));
let err = rocket::build()
.configure(Config::debug_default())
.mount("/", routes![one_state, two_states])
.manage(1 as u32)
.manage(1 as u8)
.ignite().await
.unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
let result = rocket::build()
.configure(Config::debug_default())
.mount("/", routes![one_state, two_states])
.manage(1 as u32)
.manage(1 as u8)
.manage(String::new())
.ignite().await;
assert!(result.is_ok());
}
#[test]
fn inner_sentinels_detected() {
use rocket::local::blocking::Client;
#[derive(Responder)]
struct MyThing<T>(T);
struct ResponderSentinel;
impl<'r, 'o: 'r> response::Responder<'r, 'o> for ResponderSentinel {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> {
todo!()
}
}
impl Sentinel for ResponderSentinel {
fn abort(_: &Rocket<Ignite>) -> bool {
true
}
}
#[get("/")]
fn route() -> MyThing<ResponderSentinel> { todo!() }
let err = Client::debug_with(routes![route]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
#[derive(Responder)]
struct Inner<T>(T);
#[get("/")]
fn inner() -> MyThing<Inner<ResponderSentinel>> { todo!() }
let err = Client::debug_with(routes![inner]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
#[get("/")]
fn inner_either() -> Either<Inner<ResponderSentinel>, ResponderSentinel> { todo!() }
let err = Client::debug_with(routes![inner_either]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 2));
#[derive(Responder)]
struct Block<T>(T);
impl<T> Sentinel for Block<T> {
fn abort(_: &Rocket<Ignite>) -> bool {
false
}
}
#[get("/")]
fn blocked() -> Block<ResponderSentinel> { todo!() }
Client::debug_with(routes![blocked]).expect("no sentinel errors");
#[get("/a")]
fn inner_b() -> Either<Inner<Block<ResponderSentinel>>, Block<ResponderSentinel>> {
todo!()
}
#[get("/b")]
fn inner_b2() -> Either<Block<Inner<ResponderSentinel>>, Block<ResponderSentinel>> {
todo!()
}
Client::debug_with(routes![inner_b, inner_b2]).expect("no sentinel errors");
#[get("/")]
fn half_b() -> Either<Inner<ResponderSentinel>, Block<ResponderSentinel>> {
todo!()
}
let err = Client::debug_with(routes![half_b]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
use rocket::response::Responder;
#[get("/")]
fn half_c<'r>() -> Either<
Inner<impl Responder<'r, 'static>>,
Result<ResponderSentinel, Inner<ResponderSentinel>>
> {
Either::Left(Inner(()))
}
let err = Client::debug_with(routes![half_c]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 2));
#[get("/")]
fn half_d<'r>() -> Either<
Inner<impl Responder<'r, 'static>>,
Result<Block<ResponderSentinel>, Inner<ResponderSentinel>>
> {
Either::Left(Inner(()))
}
let err = Client::debug_with(routes![half_d]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
// The special `Result` implementation.
type MyResult = Result<ResponderSentinel, ResponderSentinel>;
#[get("/")]
fn half_e<'r>() -> Either<Inner<impl Responder<'r, 'static>>, MyResult> {
Either::Left(Inner(()))
}
let err = Client::debug_with(routes![half_e]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
// Another specialized sentinel.
#[get("/")] fn either_route() -> Either<ResponderSentinel, ResponderSentinel> { todo!() }
let err = Client::debug_with(routes![either_route]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
#[get("/")] fn either_route2() -> Either<ResponderSentinel, ()> { todo!() }
let err = Client::debug_with(routes![either_route2]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
#[get("/")] fn either_route3() -> Either<(), ResponderSentinel> { todo!() }
let err = Client::debug_with(routes![either_route3]).unwrap_err();
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
#[get("/")] fn either_route4() -> Either<(), ()> { todo!() }
Client::debug_with(routes![either_route4]).expect("no sentinel error");
}

View File

@ -43,9 +43,13 @@ fn default_catcher(status: Status, req: &Request<'_>) -> status::Custom<String>
status::Custom(status, msg)
}
#[get("/unmanaged")]
fn unmanaged(_u8: rocket::State<'_, u8>, _string: rocket::State<'_, String>) { }
fn rocket() -> Rocket<Build> {
rocket::build()
// .mount("/", routes![hello, hello]) // uncoment this to get an error
// .mount("/", routes![hello, hello]) // uncomment this to get an error
// .mount("/", routes![unmanaged]) // uncomment this to get a sentinel error
.mount("/", routes![hello, forced_error])
.register("/", catchers![general_not_found, default_catcher])
.register("/hello", catchers![hello_not_found])
@ -56,6 +60,7 @@ fn rocket() -> Rocket<Build> {
async fn main() {
if let Err(e) = rocket().launch().await {
println!("Whoops! Rocket didn't launch!");
println!("Error: {:?}", e);
// We drop the error to get a Rocket-formatted panic.
drop(e);
};
}