Add default catchers: '#[catch(default)]'.

The bulk of the changes in this commit are for creating an
'ErrorHandler' trait that works like the 'Handler' trait, but for
errors. Furthermore, Rocket's default catcher now responds with a JSON
payload if the preferred 'Accept' media type is JSON.

This commit also fixes a bug in 'LocalRequest' where the internal
'Request' contained an correct 'URI'.
This commit is contained in:
Sergio Benitez 2020-07-29 23:07:22 -07:00
parent e531770989
commit 45b4436ed3
18 changed files with 665 additions and 370 deletions

View File

@ -1,8 +1,8 @@
use devise::{syn, Spanned, Result, FromMeta, Diagnostic};
use devise::ext::SpanDiagnosticExt; use devise::ext::SpanDiagnosticExt;
use devise::{syn, MetaItem, Spanned, Result, FromMeta, Diagnostic};
use crate::http_codegen::{self, Optional};
use crate::proc_macro2::{TokenStream, Span}; use crate::proc_macro2::{TokenStream, Span};
use crate::http_codegen::Status;
use crate::syn_ext::{IdentExt, ReturnTypeExt, TokenStreamExt}; use crate::syn_ext::{IdentExt, ReturnTypeExt, TokenStreamExt};
use self::syn::{Attribute, parse::Parser}; use self::syn::{Attribute, parse::Parser};
use crate::{CATCH_FN_PREFIX, CATCH_STRUCT_PREFIX}; use crate::{CATCH_FN_PREFIX, CATCH_STRUCT_PREFIX};
@ -11,13 +11,35 @@ use crate::{CATCH_FN_PREFIX, CATCH_STRUCT_PREFIX};
#[derive(Debug, FromMeta)] #[derive(Debug, FromMeta)]
struct CatchAttribute { struct CatchAttribute {
#[meta(naked)] #[meta(naked)]
status: Status status: CatcherCode
} }
/// This structure represents the parsed `catch` attribute an associated items. /// `Some` if there's a code, `None` if it's `default`.
#[derive(Debug)]
struct CatcherCode(Option<http_codegen::Status>);
impl FromMeta for CatcherCode {
fn from_meta(m: MetaItem<'_>) -> Result<Self> {
if usize::from_meta(m).is_ok() {
let status = http_codegen::Status::from_meta(m)?;
Ok(CatcherCode(Some(status)))
} else if let MetaItem::Path(path) = m {
if path.is_ident("default") {
Ok(CatcherCode(None))
} else {
Err(m.span().error(format!("expected `default`")))
}
} else {
let msg = format!("expected integer or identifier, found {}", m.description());
Err(m.span().error(msg))
}
}
}
/// This structure represents the parsed `catch` attribute and associated items.
struct CatchParams { struct CatchParams {
/// The status associated with the code in the `#[catch(code)]` attribute. /// The status associated with the code in the `#[catch(code)]` attribute.
status: Status, status: Option<http_codegen::Status>,
/// The function that was decorated with the `catch` attribute. /// The function that was decorated with the `catch` attribute.
function: syn::ItemFn, function: syn::ItemFn,
} }
@ -33,13 +55,14 @@ fn parse_params(
let full_attr = quote!(#[catch(#args)]); let full_attr = quote!(#[catch(#args)]);
let attrs = Attribute::parse_outer.parse2(full_attr)?; let attrs = Attribute::parse_outer.parse2(full_attr)?;
let attribute = match CatchAttribute::from_attrs("catch", &attrs) { let attribute = match CatchAttribute::from_attrs("catch", &attrs) {
Some(result) => result.map_err(|d| { Some(result) => result.map_err(|diag| {
d.help("`#[catch]` expects a single status integer, e.g.: #[catch(404)]") diag.help("`#[catch]` expects a status code int or `default`: \
`#[catch(404)]` or `#[catch(default)]`")
})?, })?,
None => return Err(Span::call_site().error("internal error: bad attribute")) None => return Err(Span::call_site().error("internal error: bad attribute"))
}; };
Ok(CatchParams { status: attribute.status, function }) Ok(CatchParams { status: attribute.status.0, function })
} }
pub fn _catch( pub fn _catch(
@ -54,59 +77,41 @@ pub fn _catch(
let user_catcher_fn_name = catch.function.sig.ident.clone(); let user_catcher_fn_name = catch.function.sig.ident.clone();
let generated_struct_name = user_catcher_fn_name.prepend(CATCH_STRUCT_PREFIX); let generated_struct_name = user_catcher_fn_name.prepend(CATCH_STRUCT_PREFIX);
let generated_fn_name = user_catcher_fn_name.prepend(CATCH_FN_PREFIX); let generated_fn_name = user_catcher_fn_name.prepend(CATCH_FN_PREFIX);
let (vis, status) = (&catch.function.vis, &catch.status); let (vis, catcher_status) = (&catch.function.vis, &catch.status);
let status_code = status.0.code; let status_code = Optional(catcher_status.as_ref().map(|s| s.0.code));
// Variables names we'll use and reuse. // Variables names we'll use and reuse.
define_vars_and_mods!(catch.function.span().into() => define_vars_and_mods!(catch.function.span().into() =>
req, _Box, Request, Response, CatcherFuture); req, status, _Box, Request, Response, ErrorHandlerFuture, Status);
// Determine the number of parameters that will be passed in. // Determine the number of parameters that will be passed in.
if catch.function.sig.inputs.len() > 1 { if catch.function.sig.inputs.len() > 2 {
return Err(catch.function.sig.paren_token.span return Err(catch.function.sig.paren_token.span
.error("invalid number of arguments: must be zero or one") .error("invalid number of arguments: must be zero, one, or two")
.help("catchers may optionally take an argument of type `&Request`")); .help("catchers optionally take `&Request` or `Status, &Request`"));
} }
// TODO: It would be nice if this worked! Alas, either there is a rustc bug
// that prevents this from working (error on `Output` type of `Future`), or
// this simply isn't possible with `async fn`.
// // Typecheck the catcher function if it has arguments.
// user_catcher_fn_name.set_span(catch.function.sig.paren_token.span.into());
// let user_catcher_fn_call = catch.function.sig.inputs.first()
// .map(|arg| {
// let ty = quote!(fn(&#Request) -> _).respanned(Span::call_site().into());
// let req = req.respanned(arg.span().into());
// quote!({
// let #user_catcher_fn_name: #ty = #user_catcher_fn_name;
// #user_catcher_fn_name(#req)
// })
// })
// .unwrap_or_else(|| quote!(#user_catcher_fn_name()));
//
// let catcher_response = quote_spanned!(return_type_span => {
// let ___responder = #user_catcher_fn_call #dot_await;
// ::rocket::response::Responder::respond_to(___responder, #req)?
// });
// This ensures that "Responder not implemented" points to the return type. // This ensures that "Responder not implemented" points to the return type.
let return_type_span = catch.function.sig.output.ty() let return_type_span = catch.function.sig.output.ty()
.map(|ty| ty.span().into()) .map(|ty| ty.span().into())
.unwrap_or(Span::call_site().into()); .unwrap_or(Span::call_site().into());
// Set the `req` span to that of the arg for a correct `Wrong type` span. // Set the `req` and `status` spans to that of their respective function
let input = catch.function.sig.inputs.first() // arguments for a more correct `wrong type` error span. `rev` to be cute.
.map(|arg| match arg { let codegen_args = &[&req, &status];
syn::FnArg::Receiver(_) => req.respanned(arg.span()), let inputs = catch.function.sig.inputs.iter().rev()
syn::FnArg::Typed(a) => req.respanned(a.ty.span()) .zip(codegen_args.into_iter())
}); .map(|(fn_arg, codegen_arg)| match fn_arg {
syn::FnArg::Receiver(_) => codegen_arg.respanned(fn_arg.span()),
syn::FnArg::Typed(a) => codegen_arg.respanned(a.ty.span())
}).rev();
// We append `.await` to the function call if this is `async`. // We append `.await` to the function call if this is `async`.
let dot_await = catch.function.sig.asyncness let dot_await = catch.function.sig.asyncness
.map(|a| quote_spanned!(a.span().into() => .await)); .map(|a| quote_spanned!(a.span().into() => .await));
let catcher_response = quote_spanned!(return_type_span => { let catcher_response = quote_spanned!(return_type_span => {
let ___responder = #user_catcher_fn_name(#input) #dot_await; let ___responder = #user_catcher_fn_name(#(#inputs),*) #dot_await;
::rocket::response::Responder::respond_to(___responder, #req)? ::rocket::response::Responder::respond_to(___responder, #req)?
}); });
@ -116,7 +121,10 @@ pub fn _catch(
/// Rocket code generated wrapping catch function. /// Rocket code generated wrapping catch function.
#[doc(hidden)] #[doc(hidden)]
#vis fn #generated_fn_name<'_b>(#req: &'_b #Request) -> #CatcherFuture<'_b> { #vis fn #generated_fn_name<'_b>(
#status: #Status,
#req: &'_b #Request
) -> #ErrorHandlerFuture<'_b> {
#_Box::pin(async move { #_Box::pin(async move {
let __response = #catcher_response; let __response = #catcher_response;
#Response::build() #Response::build()
@ -129,8 +137,8 @@ pub fn _catch(
/// Rocket code generated static catcher info. /// Rocket code generated static catcher info.
#[doc(hidden)] #[doc(hidden)]
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
#vis static #generated_struct_name: ::rocket::StaticCatchInfo = #vis static #generated_struct_name: ::rocket::StaticCatcherInfo =
::rocket::StaticCatchInfo { ::rocket::StaticCatcherInfo {
code: #status_code, code: #status_code,
handler: #generated_fn_name, handler: #generated_fn_name,
}; };

View File

@ -74,6 +74,7 @@ macro_rules! vars_and_mods {
vars_and_mods! { vars_and_mods! {
req => __req, req => __req,
status => __status,
catcher => __catcher, catcher => __catcher,
data => __data, data => __data,
error => __error, error => __error,
@ -92,8 +93,9 @@ vars_and_mods! {
Data => rocket::data::Data, Data => rocket::data::Data,
StaticRouteInfo => rocket::StaticRouteInfo, StaticRouteInfo => rocket::StaticRouteInfo,
SmallVec => rocket::http::private::SmallVec, SmallVec => rocket::http::private::SmallVec,
Status => rocket::http::Status,
HandlerFuture => rocket::handler::HandlerFuture, HandlerFuture => rocket::handler::HandlerFuture,
CatcherFuture => rocket::handler::CatcherFuture, ErrorHandlerFuture => rocket::catcher::ErrorHandlerFuture,
_Option => ::std::option::Option, _Option => ::std::option::Option,
_Result => ::std::result::Result, _Result => ::std::result::Result,
_Some => ::std::option::Option::Some, _Some => ::std::option::Option::Some,
@ -350,11 +352,17 @@ route_attribute!(options => Method::Options);
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
/// # /// #
/// use rocket::Request; /// use rocket::Request;
/// use rocket::http::Status;
/// ///
/// #[catch(404)] /// #[catch(404)]
/// fn not_found(req: &Request) -> String { /// fn not_found(req: &Request) -> String {
/// format!("Sorry, {} does not exist.", req.uri()) /// format!("Sorry, {} does not exist.", req.uri())
/// } /// }
///
/// #[catch(default)]
/// fn default(status: Status, req: &Request) -> String {
/// format!("{} - {} ({})", status.code, status.reason, req.uri())
/// }
/// ``` /// ```
/// ///
/// # Grammar /// # Grammar
@ -362,19 +370,19 @@ route_attribute!(options => Method::Options);
/// The grammar for the `#[catch]` attributes is defined as: /// The grammar for the `#[catch]` attributes is defined as:
/// ///
/// ```text /// ```text
/// catch := STATUS /// catch := STATUS | 'default'
/// ///
/// STATUS := valid HTTP status code (integer in [200, 599]) /// STATUS := valid HTTP status code (integer in [200, 599])
/// ``` /// ```
/// ///
/// # Typing Requirements /// # Typing Requirements
/// ///
/// The decorated function must take exactly zero or one argument. If the /// The decorated function may take zero, one, or two arguments. It's type
/// decorated function takes an argument, the argument's type must be /// signature must be one of the following, where `R:`[`Responder`]:
/// [`&Request`].
/// ///
/// The return type of the decorated function must implement the [`Responder`] /// * `fn() -> R`
/// trait. /// * `fn(`[`&Request`]`) -> R`
/// * `fn(`[`Status`]`, `[`&Request`]`) -> R`
/// ///
/// # Semantics /// # Semantics
/// ///
@ -383,16 +391,18 @@ route_attribute!(options => Method::Options);
/// 1. An [`ErrorHandler`]. /// 1. An [`ErrorHandler`].
/// ///
/// The generated handler calls the decorated function, passing in the /// The generated handler calls the decorated function, passing in the
/// [`&Request`] value if requested. The returned value is used to generate /// [`Status`] and [`&Request`] values if requested. The returned value is
/// a [`Response`] via the type's [`Responder`] implementation. /// used to generate a [`Response`] via the type's [`Responder`]
/// implementation.
/// ///
/// 2. A static structure used by [`catchers!`] to generate a [`Catcher`]. /// 2. A static structure used by [`catchers!`] to generate a [`Catcher`].
/// ///
/// The static structure (and resulting [`Catcher`]) is populated /// The static structure (and resulting [`Catcher`]) is populated with the
/// with the name (the function's name) and status code from the /// name (the function's name) and status code from the route attribute or
/// route attribute. The handler is set to the generated handler. /// `None` if `default`. The handler is set to the generated handler.
/// ///
/// [`&Request`]: ../rocket/struct.Request.html /// [`&Request`]: ../rocket/struct.Request.html
/// [`Status`]: ../rocket/http/struct.Status.html
/// [`ErrorHandler`]: ../rocket/type.ErrorHandler.html /// [`ErrorHandler`]: ../rocket/type.ErrorHandler.html
/// [`catchers!`]: macro.catchers.html /// [`catchers!`]: macro.catchers.html
/// [`Catcher`]: ../rocket/struct.Catcher.html /// [`Catcher`]: ../rocket/struct.Catcher.html
@ -842,6 +852,9 @@ pub fn routes(input: TokenStream) -> TokenStream {
/// #[catch(400)] /// #[catch(400)]
/// pub fn unauthorized() { /* .. */ } /// pub fn unauthorized() { /* .. */ }
/// } /// }
///
/// #[catch(default)]
/// fn default_catcher() { /* .. */ }
/// ``` /// ```
/// ///
/// The `catchers!` macro can be used as: /// The `catchers!` macro can be used as:
@ -850,18 +863,21 @@ pub fn routes(input: TokenStream) -> TokenStream {
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
/// # /// #
/// # #[catch(404)] fn not_found() { /* .. */ } /// # #[catch(404)] fn not_found() { /* .. */ }
/// # #[catch(default)] fn default_catcher() { /* .. */ }
/// # mod inner { /// # mod inner {
/// # #[catch(400)] pub fn unauthorized() { /* .. */ } /// # #[catch(400)] pub fn unauthorized() { /* .. */ }
/// # } /// # }
/// # /// let my_catchers = catchers![not_found, inner::unauthorized, default_catcher];
/// let my_catchers = catchers![not_found, inner::unauthorized]; /// assert_eq!(my_catchers.len(), 3);
/// assert_eq!(my_catchers.len(), 2);
/// ///
/// let not_found = &my_catchers[0]; /// let not_found = &my_catchers[0];
/// assert_eq!(not_found.code, 404); /// assert_eq!(not_found.code, Some(404));
/// ///
/// let unauthorized = &my_catchers[1]; /// let unauthorized = &my_catchers[1];
/// assert_eq!(unauthorized.code, 400); /// assert_eq!(unauthorized.code, Some(400));
///
/// let default = &my_catchers[2];
/// assert_eq!(default.code, None);
/// ``` /// ```
/// ///
/// The grammar for `catchers!` is defined as: /// The grammar for `catchers!` is defined as:

View File

@ -14,13 +14,13 @@ error: expected `fn`
| |
= help: `#[catch]` can only be used on functions = help: `#[catch]` can only be used on functions
error: invalid value: expected unsigned integer literal error: expected integer or identifier, found string literal
--> $DIR/catch.rs:11:9 --> $DIR/catch.rs:11:9
| |
11 | #[catch("404")] 11 | #[catch("404")]
| ^^^^^ | ^^^^^
| |
= help: `#[catch]` expects a single status integer, e.g.: #[catch(404)] = help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]`
error: unexpected keyed parameter: expected literal or identifier error: unexpected keyed parameter: expected literal or identifier
--> $DIR/catch.rs:14:9 --> $DIR/catch.rs:14:9
@ -28,7 +28,7 @@ error: unexpected keyed parameter: expected literal or identifier
14 | #[catch(code = "404")] 14 | #[catch(code = "404")]
| ^^^^^^^^^^^^ | ^^^^^^^^^^^^
| |
= help: `#[catch]` expects a single status integer, e.g.: #[catch(404)] = help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]`
error: unexpected keyed parameter: expected literal or identifier error: unexpected keyed parameter: expected literal or identifier
--> $DIR/catch.rs:17:9 --> $DIR/catch.rs:17:9
@ -36,7 +36,7 @@ error: unexpected keyed parameter: expected literal or identifier
17 | #[catch(code = 404)] 17 | #[catch(code = 404)]
| ^^^^^^^^^^ | ^^^^^^^^^^
| |
= help: `#[catch]` expects a single status integer, e.g.: #[catch(404)] = help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]`
error: status must be in range [100, 599] error: status must be in range [100, 599]
--> $DIR/catch.rs:20:9 --> $DIR/catch.rs:20:9
@ -44,7 +44,7 @@ error: status must be in range [100, 599]
20 | #[catch(99)] 20 | #[catch(99)]
| ^^ | ^^
| |
= help: `#[catch]` expects a single status integer, e.g.: #[catch(404)] = help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]`
error: status must be in range [100, 599] error: status must be in range [100, 599]
--> $DIR/catch.rs:23:9 --> $DIR/catch.rs:23:9
@ -52,7 +52,7 @@ error: status must be in range [100, 599]
23 | #[catch(600)] 23 | #[catch(600)]
| ^^^ | ^^^
| |
= help: `#[catch]` expects a single status integer, e.g.: #[catch(404)] = help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]`
error: unexpected attribute parameter: `message` error: unexpected attribute parameter: `message`
--> $DIR/catch.rs:26:14 --> $DIR/catch.rs:26:14
@ -60,20 +60,16 @@ error: unexpected attribute parameter: `message`
26 | #[catch(400, message = "foo")] 26 | #[catch(400, message = "foo")]
| ^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^
| |
= help: `#[catch]` expects a single status integer, e.g.: #[catch(404)] = help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]`
error: invalid number of arguments: must be zero or one error[E0308]: mismatched types
--> $DIR/catch.rs:30:6 --> $DIR/catch.rs:30:17
| |
30 | fn f3(_request: &Request, other: bool) { } 30 | fn f3(_request: &Request, other: bool) { }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^ expected `&rocket::Request<'_>`, found struct `rocket::http::Status`
|
= help: catchers may optionally take an argument of type `&Request`
warning: unused import: `rocket::Request` error[E0308]: mismatched types
--> $DIR/catch.rs:3:5 --> $DIR/catch.rs:30:34
| |
3 | use rocket::Request; 30 | fn f3(_request: &Request, other: bool) { }
| ^^^^^^^^^^^^^^^ | ^^^^ expected `bool`, found `&rocket::Request<'_>`
|
= note: `#[warn(unused_imports)]` on by default

View File

@ -12,59 +12,56 @@ error: expected `fn`
9 | const CATCH: &str = "Catcher"; 9 | const CATCH: &str = "Catcher";
| ^^^^^ | ^^^^^
error: invalid value: expected unsigned integer literal error: expected integer or identifier, found string literal
--- help: `#[catch]` expects a single status integer, e.g.: #[catch(404)] --- help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]`
--> $DIR/catch.rs:11:9 --> $DIR/catch.rs:11:9
| |
11 | #[catch("404")] 11 | #[catch("404")]
| ^^^^^ | ^^^^^
error: unexpected keyed parameter: expected literal or identifier error: unexpected keyed parameter: expected literal or identifier
--- help: `#[catch]` expects a single status integer, e.g.: #[catch(404)] --- help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]`
--> $DIR/catch.rs:14:9 --> $DIR/catch.rs:14:9
| |
14 | #[catch(code = "404")] 14 | #[catch(code = "404")]
| ^^^^ | ^^^^
error: unexpected keyed parameter: expected literal or identifier error: unexpected keyed parameter: expected literal or identifier
--- help: `#[catch]` expects a single status integer, e.g.: #[catch(404)] --- help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]`
--> $DIR/catch.rs:17:9 --> $DIR/catch.rs:17:9
| |
17 | #[catch(code = 404)] 17 | #[catch(code = 404)]
| ^^^^ | ^^^^
error: status must be in range [100, 599] error: status must be in range [100, 599]
--- help: `#[catch]` expects a single status integer, e.g.: #[catch(404)] --- help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]`
--> $DIR/catch.rs:20:9 --> $DIR/catch.rs:20:9
| |
20 | #[catch(99)] 20 | #[catch(99)]
| ^^ | ^^
error: status must be in range [100, 599] error: status must be in range [100, 599]
--- help: `#[catch]` expects a single status integer, e.g.: #[catch(404)] --- help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]`
--> $DIR/catch.rs:23:9 --> $DIR/catch.rs:23:9
| |
23 | #[catch(600)] 23 | #[catch(600)]
| ^^^ | ^^^
error: unexpected attribute parameter: `message` error: unexpected attribute parameter: `message`
--- help: `#[catch]` expects a single status integer, e.g.: #[catch(404)] --- help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]`
--> $DIR/catch.rs:26:14 --> $DIR/catch.rs:26:14
| |
26 | #[catch(400, message = "foo")] 26 | #[catch(400, message = "foo")]
| ^^^^^^^ | ^^^^^^^
error: invalid number of arguments: must be zero or one error[E0308]: mismatched types
--- help: catchers may optionally take an argument of type `&Request` --> $DIR/catch.rs:30:17
--> $DIR/catch.rs:30:6
| |
30 | fn f3(_request: &Request, other: bool) { } 30 | fn f3(_request: &Request, other: bool) { }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^ expected `&rocket::Request<'_>`, found struct `rocket::http::Status`
warning: unused import: `rocket::Request` error[E0308]: mismatched types
--> $DIR/catch.rs:3:5 --> $DIR/catch.rs:30:34
| |
3 | use rocket::Request; 30 | fn f3(_request: &Request, other: bool) { }
| ^^^^^^^^^^^^^^^ | ^^^^ expected `bool`, found `&rocket::Request<'_>`
|
= note: `#[warn(unused_imports)]` on by default

View File

@ -154,7 +154,7 @@ impl Status {
/// assert_eq!(custom.to_string(), "299 Somewhat Successful".to_string()); /// assert_eq!(custom.to_string(), "299 Somewhat Successful".to_string());
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn new(code: u16, reason: &'static str) -> Status { pub const fn new(code: u16, reason: &'static str) -> Status {
Status { code, reason } Status { code, reason }
} }
@ -184,7 +184,7 @@ impl Status {
/// let custom = Status::new(600, "Bizarre"); /// let custom = Status::new(600, "Bizarre");
/// assert_eq!(custom.class(), StatusClass::Unknown); /// assert_eq!(custom.class(), StatusClass::Unknown);
/// ``` /// ```
pub fn class(&self) -> StatusClass { pub fn class(self) -> StatusClass {
match self.code / 100 { match self.code / 100 {
1 => StatusClass::Informational, 1 => StatusClass::Informational,
2 => StatusClass::Success, 2 => StatusClass::Success,

View File

@ -1,44 +1,67 @@
use std::future::Future; //! Types and traits for error catchers, error handlers, and their return
//! values.
use crate::response;
use crate::handler::ErrorHandler;
use crate::codegen::StaticCatchInfo;
use crate::request::Request;
use std::fmt; use std::fmt;
use yansi::Color::*;
use crate::response::Response;
use crate::codegen::StaticCatcherInfo;
use crate::request::Request;
use futures::future::BoxFuture;
use yansi::Paint;
/// Type alias for the return value of an [`ErrorHandler`]. For now, identical
/// to [`response::Result`](crate::response::Result).
pub type Result<'r> = std::result::Result<Response<'r>, crate::http::Status>;
/// Type alias for the unwieldy [`ErrorHandler::handle()`] return type.
pub type ErrorHandlerFuture<'r> = BoxFuture<'r, Result<'r>>;
/// An error catching route. /// An error catching route.
/// ///
/// Catchers are routes that run when errors occur. They correspond directly /// # Overview
/// with the HTTP error status code they will be handling and are registered
/// with Rocket via [`Rocket::register()`](crate::Rocket::register()). For example,
/// to handle "404 not found" errors, a catcher for the "404" status code is
/// registered.
/// ///
/// Because error handlers are only called when all routes are exhausted, they /// Catchers are routes that run when errors are produced by the application.
/// should not fail nor forward. If an error catcher fails, the user will /// They consist of an [`ErrorHandler`] and an optional status code to match
/// receive no response. If an error catcher forwards, Rocket will respond with /// against arising errors. Errors arise from the the following sources:
/// an internal server error.
/// ///
/// # Built-In Catchers /// * A failing guard.
/// * A failing responder.
/// * Routing failure.
/// ///
/// Rocket has many built-in, pre-registered default catchers. In particular, /// Each failure is paired with a status code. Guards and responders indicate
/// Rocket has catchers for all of the following status codes: 400, 401, 402, /// the status code themselves via their `Err` return value while a routing
/// 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, /// failure is always a `404`. Rocket invokes the error handler for the catcher
/// 418, 421, 426, 428, 429, 431, 451, 500, 501, 503, and 510. As such, catchers /// with the error's status code.
/// only need to be registered if an error needs to be handled in a custom ///
/// fashion. /// ## Default Catchers
///
/// If no catcher for a given status code exists, the _default_ catcher is
/// called. A _default_ catcher is a `Catcher` with a `code` of `None`. There is
/// at-most one default catcher.
///
/// ## Error Handler Restrictions
///
/// Because error handlers are a last resort, they should not fail to produce a
/// response. If an error handler _does_ fail, Rocket invokes its default `500`
/// error catcher. Error handlers cannot forward.
///
/// # Built-In Default Catcher
///
/// Rocket's built-in default catcher can handle all errors. It produces HTML or
/// JSON, depending on the value of the `Accept` header. As such, catchers only
/// need to be registered if an error needs to be handled in a custom fashion.
/// ///
/// # Code Generation /// # Code Generation
/// ///
/// Catchers should rarely be used directly. Instead, they are typically /// Catchers should rarely be constructed or used directly. Instead, they are
/// declared using the `catch` decorator, as follows: /// typically generated via the [`catch`] attribute, as follows:
/// ///
/// ```rust,no_run /// ```rust,no_run
/// #[macro_use] extern crate rocket; /// #[macro_use] extern crate rocket;
/// ///
/// use rocket::Request; /// use rocket::Request;
/// use rocket::http::Status;
/// ///
/// #[catch(500)] /// #[catch(500)]
/// fn internal_error() -> &'static str { /// fn internal_error() -> &'static str {
@ -50,74 +73,204 @@ use yansi::Color::*;
/// format!("I couldn't find '{}'. Try something else?", req.uri()) /// format!("I couldn't find '{}'. Try something else?", req.uri())
/// } /// }
/// ///
/// #[catch(default)]
/// fn default(status: Status, req: &Request) -> String {
/// format!("{} - {} ({})", status.code, status.reason, req.uri())
/// }
///
/// #[launch] /// #[launch]
/// fn rocket() -> rocket::Rocket { /// fn rocket() -> rocket::Rocket {
/// rocket::ignite().register(catchers![internal_error, not_found]) /// rocket::ignite().register(catchers![internal_error, not_found, default])
/// } /// }
/// ``` /// ```
/// ///
/// A function decorated with `catch` must take exactly zero or one arguments. /// A function decorated with `#[catch]` may take zero, one, or two arguments.
/// If the catcher takes an argument, it must be of type [`&Request`](Request). /// It's type signature must be one of the following, where `R:`[`Responder`]:
///
/// * `fn() -> R`
/// * `fn(`[`&Request`]`) -> R`
/// * `fn(`[`Status`]`, `[`&Request`]`) -> R`
///
/// See the [`catch`] documentation for full details.
///
/// [`catch`]: rocket_codegen::catch
/// [`Responder`]: crate::response::Responder
/// [`&Request`]: crate::request::Request
/// [`Status`]: crate::http::Status
#[derive(Clone)]
pub struct Catcher { pub struct Catcher {
/// The HTTP status code to match against. /// The HTTP status code to match against if this route is not `default`.
pub code: u16, pub code: Option<u16>,
/// The catcher's associated handler.
pub handler: ErrorHandler, /// The catcher's associated error handler.
pub(crate) is_default: bool, pub handler: Box<dyn ErrorHandler>,
} }
impl Catcher { impl Catcher {
/// Creates a catcher for the given status code using the given error /// Creates a catcher for the given status code, or a default catcher if
/// handler. This should only be used when routing manually. /// `code` is `None`, using the given error handler. This should only be
/// used when routing manually.
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # #![allow(unused_variables)] /// use rocket::request::Request;
/// use rocket::{Catcher, Request}; /// use rocket::catcher::{Catcher, ErrorHandlerFuture};
/// use rocket::handler::CatcherFuture; /// use rocket::response::{Result, Responder, status::Custom};
/// use rocket::response::{Result, Responder};
/// use rocket::response::status::Custom;
/// use rocket::http::Status; /// use rocket::http::Status;
/// ///
/// fn handle_404<'r>(req: &'r Request) -> CatcherFuture<'r> { /// fn handle_404<'r>(status: Status, req: &'r Request<'_>) -> ErrorHandlerFuture<'r> {
/// let res = Custom(Status::NotFound, format!("404: {}", req.uri())); /// let res = Custom(status, format!("404: {}", req.uri()));
/// Box::pin(async move { res.respond_to(req) }) /// Box::pin(async move { res.respond_to(req) })
/// } /// }
/// ///
/// fn handle_500<'r>(req: &'r Request) -> CatcherFuture<'r> { /// fn handle_500<'r>(_: Status, req: &'r Request<'_>) -> ErrorHandlerFuture<'r> {
/// Box::pin(async move{ "Whoops, we messed up!".respond_to(req) }) /// Box::pin(async move{ "Whoops, we messed up!".respond_to(req) })
/// } /// }
/// ///
/// fn handle_default<'r>(status: Status, req: &'r Request<'_>) -> ErrorHandlerFuture<'r> {
/// let res = Custom(status, format!("{}: {}", status, req.uri()));
/// Box::pin(async move { res.respond_to(req) })
/// }
///
/// let not_found_catcher = Catcher::new(404, handle_404); /// let not_found_catcher = Catcher::new(404, handle_404);
/// let internal_server_error_catcher = Catcher::new(500, handle_500); /// let internal_server_error_catcher = Catcher::new(500, handle_500);
/// let default_error_catcher = Catcher::new(None, handle_default);
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn new(code: u16, handler: ErrorHandler) -> Catcher { pub fn new<C, H>(code: C, handler: H) -> Catcher
Catcher { code, handler, is_default: false } where C: Into<Option<u16>>, H: ErrorHandler
{
Catcher { code: code.into(), handler: Box::new(handler) }
} }
}
#[inline(always)] impl Default for Catcher {
pub(crate) fn handle<'r>(&self, req: &'r Request<'_>) -> impl Future<Output = response::Result<'r>> { fn default() -> Self {
(self.handler)(req) fn async_default<'r>(status: Status, request: &'r Request<'_>) -> ErrorHandlerFuture<'r> {
Box::pin(async move { default(status, request) })
}
Catcher { code: None, handler: Box::new(async_default) }
} }
}
/// Trait implemented by types that can handle errors.
///
/// This trait is exactly like [`Handler`](crate::handler::Handler) except it
/// handles error instead of requests. We defer to its documentation.
///
/// ## Async Trait
///
/// This is an _async_ trait. Implementations must be decorated
/// [`#[rocket::async_trait]`](crate::async_trait).
///
/// # Example
///
/// Say you'd like to write a handler that changes its functionality based on a
/// `Kind` enum value that the user provides. Such a handler might be written
/// and used as follows:
///
/// ```rust,no_run
/// use rocket::{Request, Catcher};
/// use rocket::catcher::{self, ErrorHandler};
/// use rocket::response::{Response, Responder};
/// use rocket::http::Status;
///
/// #[derive(Copy, Clone)]
/// enum Kind {
/// Simple,
/// Intermediate,
/// Complex,
/// }
///
/// #[derive(Clone)]
/// struct CustomHandler(Kind);
///
/// #[rocket::async_trait]
/// impl ErrorHandler for CustomHandler {
/// async fn handle<'r, 's: 'r>(
/// &'s self,
/// status: Status,
/// req: &'r Request<'_>
/// ) -> catcher::Result<'r> {
/// let inner = match self.0 {
/// Kind::Simple => "simple".respond_to(req)?,
/// Kind::Intermediate => "intermediate".respond_to(req)?,
/// Kind::Complex => "complex".respond_to(req)?,
/// };
///
/// Response::build_from(inner).status(status).ok()
/// }
/// }
///
/// impl CustomHandler {
/// /// Returns a `default` catcher that uses `CustomHandler`.
/// fn default(kind: Kind) -> Vec<Catcher> {
/// vec![Catcher::new(None, CustomHandler(kind))]
/// }
///
/// /// Returns a catcher for code `status` that uses `CustomHandler`.
/// fn catch(status: Status, kind: Kind) -> Vec<Catcher> {
/// vec![Catcher::new(status.code, CustomHandler(kind))]
/// }
/// }
///
/// #[rocket::launch]
/// fn rocket() -> rocket::Rocket {
/// rocket::ignite()
/// // to handle only `404`
/// .register(CustomHandler::catch(Status::NotFound, Kind::Simple))
/// // or to register as the default
/// .register(CustomHandler::default(Kind::Simple))
/// }
/// ```
///
/// Note the following:
///
/// 1. `CustomHandler` implements `Clone`. This is required so that
/// `CustomHandler` implements `Cloneable` automatically. The `Cloneable`
/// trait serves no other purpose but to ensure that every `ErrorHandler`
/// can be cloned, allowing `Catcher`s to be cloned.
/// 2. `CustomHandler`'s methods return `Vec<Route>`, allowing for use
/// directly as the parameter to `rocket.register()`.
/// 3. Unlike static-function-based handlers, this custom handler can make use
/// of internal state.
#[crate::async_trait]
pub trait ErrorHandler: Cloneable + Send + Sync + 'static {
/// Called by Rocket when an error with `status` for a given `Request`
/// should be handled by this handler.
///
/// Error handlers _should not_ fail and thus _should_ always return `Ok`.
/// Nevertheless, failure is allowed, both for convenience and necessity. If
/// an error handler fails, Rocket's default `500` catcher is invoked. If it
/// succeeds, the returned `Response` is used to respond to the client.
async fn handle<'r, 's: 'r>(&'s self, status: Status, req: &'r Request<'_>) -> Result<'r>;
}
#[crate::async_trait]
impl<F: Clone + Sync + Send + 'static> ErrorHandler for F
where for<'x> F: Fn(Status, &'x Request<'_>) -> ErrorHandlerFuture<'x>
{
#[inline(always)] #[inline(always)]
fn new_default(code: u16, handler: ErrorHandler) -> Catcher { async fn handle<'r, 's: 'r>(&'s self, status: Status, req: &'r Request<'_>) -> Result<'r> {
Catcher { code, handler, is_default: true, } self(status, req).await
} }
} }
#[doc(hidden)] #[doc(hidden)]
impl<'a> From<&'a StaticCatchInfo> for Catcher { impl<'a> From<&'a StaticCatcherInfo> for Catcher {
fn from(info: &'a StaticCatchInfo) -> Catcher { fn from(info: &'a StaticCatcherInfo) -> Catcher {
Catcher::new(info.code, info.handler) Catcher::new(info.code, info.handler)
} }
} }
impl fmt::Display for Catcher { impl fmt::Display for Catcher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", Blue.paint(&self.code)) match self.code {
Some(code) => write!(f, "{}", Paint::blue(code)),
None => write!(f, "{}", Paint::blue("default"))
}
} }
} }
@ -125,23 +278,22 @@ impl fmt::Debug for Catcher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Catcher") f.debug_struct("Catcher")
.field("code", &self.code) .field("code", &self.code)
.field("default", &self.is_default)
.finish() .finish()
} }
} }
macro_rules! error_page_template { macro_rules! html_error_template {
($code:expr, $name:expr, $description:expr) => ( ($code:expr, $reason:expr, $description:expr) => (
concat!(r#" concat!(r#"
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>"#, $code, " ", $name, r#"</title> <title>"#, $code, " ", $reason, r#"</title>
</head> </head>
<body align="center"> <body align="center">
<div role="main" align="center"> <div role="main" align="center">
<h1>"#, $code, ": ", $name, r#"</h1> <h1>"#, $code, ": ", $reason, r#"</h1>
<p>"#, $description, r#"</p> <p>"#, $description, r#"</p>
<hr /> <hr />
</div> </div>
@ -155,98 +307,136 @@ macro_rules! error_page_template {
) )
} }
macro_rules! default_catchers { macro_rules! json_error_template {
($($code:expr, $name:expr, $description:expr, $fn_name:ident),+) => ( ($code:expr, $reason:expr, $description:expr) => (
let mut map = HashMap::new(); concat!(
r#"{
$( "error": {
fn $fn_name<'r>(req: &'r Request<'_>) -> crate::handler::CatcherFuture<'r> { "code": "#, $code, r#",
let status = Status::from_code($code).unwrap(); "reason": ""#, $reason, r#"",
let html = content::Html(error_page_template!($code, $name, $description)); "description": ""#, $description, r#""
Box::pin(async move { status::Custom(status, html).respond_to(req) }) }
} }"#
)
map.insert($code, Catcher::new_default($code, $fn_name));
)+
map
) )
} }
pub mod defaults { // This is unfortunate, but the `{`, `}` above make it unusable for `format!`.
use super::Catcher; macro_rules! json_error_fmt_template {
($code:expr, $reason:expr, $description:expr) => (
concat!(
r#"{{
"error": {{
"code": "#, $code, r#",
"reason": ""#, $reason, r#"",
"description": ""#, $description, r#""
}}
}}"#
)
)
}
use std::collections::HashMap; macro_rules! default_catcher_fn {
($($code:expr, $reason:expr, $description:expr),+) => (
use std::borrow::Cow;
use crate::http::Status;
use crate::response::{content, status, Responder};
use crate::request::Request; pub(crate) fn default<'r>(status: Status, req: &'r Request<'_>) -> Result<'r> {
use crate::response::{content, status, Responder}; if req.accept().map(|a| a.preferred().is_json()).unwrap_or(false) {
use crate::http::Status; let json: Cow<'_, str> = match status.code {
$($code => json_error_template!($code, $reason, $description).into(),)*
code => format!(json_error_fmt_template!("{}", "Unknown Error",
"An unknown error has occurred."), code).into()
};
pub fn get() -> HashMap<u16, Catcher> { status::Custom(status, content::Json(json)).respond_to(req)
default_catchers! { } else {
400, "Bad Request", "The request could not be understood by the server due let html: Cow<'_, str> = match status.code {
to malformed syntax.", handle_400, $($code => html_error_template!($code, $reason, $description).into(),)*
401, "Unauthorized", "The request requires user authentication.", code => format!(html_error_template!("{}", "Unknown Error",
handle_401, "An unknown error has occurred."), code, code).into(),
402, "Payment Required", "The request could not be processed due to lack of };
payment.", handle_402,
403, "Forbidden", "The server refused to authorize the request.", handle_403, status::Custom(status, content::Html(html)).respond_to(req)
404, "Not Found", "The requested resource could not be found.", handle_404, }
405, "Method Not Allowed", "The request method is not supported for the
requested resource.", handle_405,
406, "Not Acceptable", "The requested resource is capable of generating
only content not acceptable according to the Accept headers sent in the
request.", handle_406,
407, "Proxy Authentication Required", "Authentication with the proxy is
required.", handle_407,
408, "Request Timeout", "The server timed out waiting for the
request.", handle_408,
409, "Conflict", "The request could not be processed because of a conflict
in the request.", handle_409,
410, "Gone", "The resource requested is no longer available and will not be
available again.", handle_410,
411, "Length Required", "The request did not specify the length of its
content, which is required by the requested resource.", handle_411,
412, "Precondition Failed", "The server does not meet one of the
preconditions specified in the request.", handle_412,
413, "Payload Too Large", "The request is larger than the server is
willing or able to process.", handle_413,
414, "URI Too Long", "The URI provided was too long for the server to
process.", handle_414,
415, "Unsupported Media Type", "The request entity has a media type which
the server or resource does not support.", handle_415,
416, "Range Not Satisfiable", "The portion of the requested file cannot be
supplied by the server.", handle_416,
417, "Expectation Failed", "The server cannot meet the requirements of the
Expect request-header field.", handle_417,
418, "I'm a teapot", "I was requested to brew coffee, and I am a
teapot.", handle_418,
421, "Misdirected Request", "The server cannot produce a response for this
request.", handle_421,
422, "Unprocessable Entity", "The request was well-formed but was unable to
be followed due to semantic errors.", handle_422,
426, "Upgrade Required", "Switching to the protocol in the Upgrade header
field is required.", handle_426,
428, "Precondition Required", "The server requires the request to be
conditional.", handle_428,
429, "Too Many Requests", "Too many requests have been received
recently.", handle_429,
431, "Request Header Fields Too Large", "The server is unwilling to process
the request because either an individual header field, or all
the header fields collectively, are too large.", handle_431,
451, "Unavailable For Legal Reasons", "The requested resource is
unavailable due to a legal demand to deny access to this
resource.", handle_451,
500, "Internal Server Error", "The server encountered an internal error
while processing this request.", handle_500,
501, "Not Implemented", "The server either does not recognize the request
method, or it lacks the ability to fulfill the request.", handle_501,
503, "Service Unavailable", "The server is currently unavailable.",
handle_503,
504, "Gateway Timeout", "The server did not receive a timely
response from an upstream server.", handle_504,
510, "Not Extended", "Further extensions to the request are required for
the server to fulfill it.", handle_510
} }
)
}
default_catcher_fn! {
400, "Bad Request", "The request could not be understood by the server due \
to malformed syntax.",
401, "Unauthorized", "The request requires user authentication.",
402, "Payment Required", "The request could not be processed due to lack of payment.",
403, "Forbidden", "The server refused to authorize the request.",
404, "Not Found", "The requested resource could not be found.",
405, "Method Not Allowed", "The request method is not supported for the requested resource.",
406, "Not Acceptable", "The requested resource is capable of generating only content not \
acceptable according to the Accept headers sent in the request.",
407, "Proxy Authentication Required", "Authentication with the proxy is required.",
408, "Request Timeout", "The server timed out waiting for the request.",
409, "Conflict", "The request could not be processed because of a conflict in the request.",
410, "Gone", "The resource requested is no longer available and will not be available again.",
411, "Length Required", "The request did not specify the length of its content, which is \
required by the requested resource.",
412, "Precondition Failed", "The server does not meet one of the \
preconditions specified in the request.",
413, "Payload Too Large", "The request is larger than the server is \
willing or able to process.",
414, "URI Too Long", "The URI provided was too long for the server to process.",
415, "Unsupported Media Type", "The request entity has a media type which \
the server or resource does not support.",
416, "Range Not Satisfiable", "The portion of the requested file cannot be \
supplied by the server.",
417, "Expectation Failed", "The server cannot meet the requirements of the \
Expect request-header field.",
418, "I'm a teapot", "I was requested to brew coffee, and I am a teapot.",
421, "Misdirected Request", "The server cannot produce a response for this request.",
422, "Unprocessable Entity", "The request was well-formed but was unable to \
be followed due to semantic errors.",
426, "Upgrade Required", "Switching to the protocol in the Upgrade header field is required.",
428, "Precondition Required", "The server requires the request to be conditional.",
429, "Too Many Requests", "Too many requests have been received recently.",
431, "Request Header Fields Too Large", "The server is unwilling to process \
the request because either an individual header field, or all the header \
fields collectively, are too large.",
451, "Unavailable For Legal Reasons", "The requested resource is unavailable \
due to a legal demand to deny access to this resource.",
500, "Internal Server Error", "The server encountered an internal error while \
processing this request.",
501, "Not Implemented", "The server either does not recognize the request \
method, or it lacks the ability to fulfill the request.",
503, "Service Unavailable", "The server is currently unavailable.",
504, "Gateway Timeout", "The server did not receive a timely response from an upstream server.",
510, "Not Extended", "Further extensions to the request are required for \
the server to fulfill it."
}
// `Cloneable` implementation below.
mod private {
pub trait Sealed {}
impl<T: super::ErrorHandler + Clone> Sealed for T {}
}
/// Unfortunate but necessary hack to be able to clone a `Box<ErrorHandler>`.
///
/// This trait cannot be implemented by any type. Instead, all types that
/// implement `Clone` and `Handler` automatically implement `Cloneable`.
pub trait Cloneable: private::Sealed {
#[doc(hidden)]
fn clone_handler(&self) -> Box<dyn ErrorHandler>;
}
impl<T: ErrorHandler + Clone> Cloneable for T {
fn clone_handler(&self) -> Box<dyn ErrorHandler> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn ErrorHandler> {
fn clone(&self) -> Box<dyn ErrorHandler> {
self.clone_handler()
} }
} }

View File

@ -1,11 +1,13 @@
use crate::futures::future::BoxFuture;
use crate::{Request, Data}; use crate::{Request, Data};
use crate::handler::{Outcome, ErrorHandler}; use crate::handler::HandlerFuture;
use crate::http::{Method, MediaType}; use crate::catcher::ErrorHandlerFuture;
use crate::http::{Status, Method, MediaType};
/// Type of a static handler, which users annotate with Rocket's attribute. /// Type of a route handler, generated from a `fn` annotated with `#[route]`.
pub type StaticHandler = for<'r> fn(&'r Request<'_>, Data) -> BoxFuture<'r, Outcome<'r>>; pub type StaticHandler = for<'r> fn(&'r Request<'_>, Data) -> HandlerFuture<'r>;
/// Type of an error handler, generated from a `fn` annotated with `#[catch]`.
pub type StaticErrorHandler = for<'r> fn(Status, &'r Request<'_>) -> ErrorHandlerFuture<'r>;
/// Information generated by the `route` attribute during codegen. /// Information generated by the `route` attribute during codegen.
pub struct StaticRouteInfo { pub struct StaticRouteInfo {
@ -24,9 +26,9 @@ pub struct StaticRouteInfo {
} }
/// Information generated by the `catch` attribute during codegen. /// Information generated by the `catch` attribute during codegen.
pub struct StaticCatchInfo { pub struct StaticCatcherInfo {
/// The catcher's status code. /// The catcher's status code.
pub code: u16, pub code: Option<u16>,
/// The catcher's handler, i.e, the annotated function. /// The catcher's handler, i.e, the annotated function.
pub handler: ErrorHandler, pub handler: StaticErrorHandler,
} }

View File

@ -1,10 +1,10 @@
//! Types and traits for request and error handlers and their return values. //! Types and traits for request handlers and their return values.
use futures::future::BoxFuture; use futures::future::BoxFuture;
use crate::data::Data; use crate::data::Data;
use crate::request::Request; use crate::request::Request;
use crate::response::{self, Response, Responder}; use crate::response::{Response, Responder};
use crate::http::Status; use crate::http::Status;
use crate::outcome; use crate::outcome;
@ -151,30 +151,6 @@ pub trait Handler: Cloneable + Send + Sync + 'static {
async fn handle<'r, 's: 'r>(&'s self, request: &'r Request<'_>, data: Data) -> Outcome<'r>; async fn handle<'r, 's: 'r>(&'s self, request: &'r Request<'_>, data: Data) -> Outcome<'r>;
} }
/// Unfortunate but necessary hack to be able to clone a `Box<Handler>`.
///
/// This trait should _never_ (and cannot, due to coherence) be implemented by
/// any type. Instead, implement `Clone`. All types that implement `Clone` and
/// `Handler` automatically implement `Cloneable`.
pub trait Cloneable {
/// Clones `self`.
fn clone_handler(&self) -> Box<dyn Handler>;
}
impl<T: Handler + Clone> Cloneable for T {
#[inline(always)]
fn clone_handler(&self) -> Box<dyn Handler> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn Handler> {
#[inline(always)]
fn clone(&self) -> Box<dyn Handler> {
self.clone_handler()
}
}
#[crate::async_trait] #[crate::async_trait]
impl<F: Clone + Sync + Send + 'static> Handler for F impl<F: Clone + Sync + Send + 'static> Handler for F
where for<'x> F: Fn(&'x Request<'_>, Data) -> HandlerFuture<'x> where for<'x> F: Fn(&'x Request<'_>, Data) -> HandlerFuture<'x>
@ -185,12 +161,6 @@ impl<F: Clone + Sync + Send + 'static> Handler for F
} }
} }
/// The type of an error handler.
pub type ErrorHandler = for<'r> fn(&'r Request<'_>) -> CatcherFuture<'r>;
/// Type type of `Future` returned by an error handler.
pub type CatcherFuture<'r> = BoxFuture<'r, response::Result<'r>>;
// A handler to use when one is needed temporarily. Don't use outside of Rocket! // A handler to use when one is needed temporarily. Don't use outside of Rocket!
#[doc(hidden)] #[doc(hidden)]
pub fn dummy<'r>(r: &'r Request<'_>, _: Data) -> HandlerFuture<'r> { pub fn dummy<'r>(r: &'r Request<'_>, _: Data) -> HandlerFuture<'r> {
@ -318,3 +288,29 @@ impl<'r, 'o: 'r> Outcome<'o> {
outcome::Outcome::Forward(data) outcome::Outcome::Forward(data)
} }
} }
mod private {
pub trait Sealed {}
impl<T: super::Handler + Clone> Sealed for T {}
}
/// Unfortunate but necessary hack to be able to clone a `Box<Handler>`.
///
/// This trait cannot be implemented by any type. Instead, all types that
/// implement `Clone` and `Handler` automatically implement `Cloneable`.
pub trait Cloneable: private::Sealed {
#[doc(hidden)]
fn clone_handler(&self) -> Box<dyn Handler>;
}
impl<T: Handler + Clone> Cloneable for T {
fn clone_handler(&self) -> Box<dyn Handler> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn Handler> {
fn clone(&self) -> Box<dyn Handler> {
self.clone_handler()
}
}

View File

@ -106,6 +106,7 @@ pub mod data;
pub mod handler; pub mod handler;
pub mod fairing; pub mod fairing;
pub mod error; pub mod error;
pub mod catcher;
// Reexport of HTTP everything. // Reexport of HTTP everything.
pub mod http { pub mod http {
@ -122,16 +123,15 @@ mod shutdown;
mod router; mod router;
mod rocket; mod rocket;
mod codegen; mod codegen;
mod catcher;
mod ext; mod ext;
#[doc(inline)] pub use crate::response::Response; #[doc(inline)] pub use crate::response::Response;
#[doc(hidden)] pub use crate::codegen::{StaticRouteInfo, StaticCatchInfo}; #[doc(hidden)] pub use crate::codegen::{StaticRouteInfo, StaticCatcherInfo};
#[doc(inline)] pub use crate::data::Data; #[doc(inline)] pub use crate::data::Data;
#[doc(inline)] pub use crate::config::Config; #[doc(inline)] pub use crate::config::Config;
#[doc(inline)] pub use crate::catcher::Catcher;
pub use crate::router::Route; pub use crate::router::Route;
pub use crate::request::{Request, State}; pub use crate::request::{Request, State};
pub use crate::catcher::Catcher;
pub use crate::rocket::{Cargo, Rocket}; pub use crate::rocket::{Cargo, Rocket};
pub use crate::shutdown::Shutdown; pub use crate::shutdown::Shutdown;

View File

@ -42,8 +42,10 @@ impl<'c> LocalRequest<'c> {
method: Method, method: Method,
uri: Cow<'c, str> uri: Cow<'c, str>
) -> LocalRequest<'c> { ) -> LocalRequest<'c> {
// We set a dummy string for now and check the user's URI on dispatch. // We try to validate the URI now so that the inner `Request` contains a
let request = Request::new(client.rocket(), method, Origin::dummy()); // valid URI. If it doesn't, we set a dummy one.
let origin = Origin::parse(&uri).unwrap_or_else(|_| Origin::dummy());
let request = Request::new(client.rocket(), method, origin.into_owned());
// Set up any cookies we know about. // Set up any cookies we know about.
if let Some(ref jar) = client.cookies { if let Some(ref jar) = client.cookies {
@ -70,12 +72,11 @@ impl<'c> LocalRequest<'c> {
// Performs the actual dispatch. // Performs the actual dispatch.
async fn _dispatch(mut self) -> LocalResponse<'c> { async fn _dispatch(mut self) -> LocalResponse<'c> {
// First, validate the URI, returning an error response (generated from // First, revalidate the URI, returning an error response (generated
// an error catcher) immediately if it's invalid. // from an error catcher) immediately if it's invalid. If it's valid,
// then `request` already contains the correct URI.
let rocket = self.client.rocket(); let rocket = self.client.rocket();
if let Ok(uri) = Origin::parse(&self.uri) { if let Err(_) = Origin::parse(&self.uri) {
self.request.set_uri(uri.into_owned());
} else {
error!("Malformed request URI: {}", self.uri); error!("Malformed request URI: {}", self.uri);
return LocalResponse::new(self.request, move |req| { return LocalResponse::new(self.request, move |req| {
rocket.handle_error(Status::BadRequest, req) rocket.handle_error(Status::BadRequest, req)

View File

@ -683,6 +683,9 @@ macro_rules! try_outcome {
}); });
} }
#[doc(inline)]
pub use try_outcome;
impl<S, E, F> fmt::Debug for Outcome<S, E, F> { impl<S, E, F> fmt::Debug for Outcome<S, E, F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Outcome::{}", self.formatting().1) write!(f, "Outcome::{}", self.formatting().1)

View File

@ -258,6 +258,18 @@ impl<'r> Responder<'r, 'static> for () {
} }
} }
/// Responds with the inner `Responder` in `Cow`.
impl<'r, 'o: 'r, R: ?Sized + ToOwned> Responder<'r, 'o> for std::borrow::Cow<'o, R>
where &'o R: Responder<'r, 'o> + 'o, <R as ToOwned>::Owned: Responder<'r, 'o> + 'r
{
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> {
match self {
std::borrow::Cow::Borrowed(b) => b.respond_to(req),
std::borrow::Cow::Owned(o) => o.respond_to(req),
}
}
}
/// If `self` is `Some`, responds with the wrapped `Responder`. Otherwise prints /// If `self` is `Some`, responds with the wrapped `Responder`. Otherwise prints
/// a warning message and returns an `Err` of `Status::NotFound`. /// a warning message and returns an `Err` of `Status::NotFound`.
impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for Option<R> { impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for Option<R> {
@ -311,7 +323,6 @@ impl<'r> Responder<'r, 'static> for Status {
} }
_ => { _ => {
error_!("Invalid status used as responder: {}.", self); error_!("Invalid status used as responder: {}.", self);
warn_!("Fowarding to 500 (Internal Server Error) catcher.");
Err(Status::InternalServerError) Err(Status::InternalServerError)
} }
} }

View File

@ -17,9 +17,9 @@ use crate::{logger, handler};
use crate::config::{Config, FullConfig, ConfigError, LoggedValue}; use crate::config::{Config, FullConfig, ConfigError, LoggedValue};
use crate::request::{Request, FormItems}; use crate::request::{Request, FormItems};
use crate::data::Data; use crate::data::Data;
use crate::catcher::Catcher;
use crate::response::{Body, Response}; use crate::response::{Body, Response};
use crate::router::{Router, Route}; use crate::router::{Router, Route};
use crate::catcher::{self, Catcher};
use crate::outcome::Outcome; use crate::outcome::Outcome;
use crate::error::{LaunchError, LaunchErrorKind}; use crate::error::{LaunchError, LaunchErrorKind};
use crate::fairing::{Fairing, Fairings}; use crate::fairing::{Fairing, Fairings};
@ -39,7 +39,7 @@ pub struct Rocket {
pub(crate) managed_state: Container, pub(crate) managed_state: Container,
manifest: Vec<PreLaunchOp>, manifest: Vec<PreLaunchOp>,
router: Router, router: Router,
default_catchers: HashMap<u16, Catcher>, default_catcher: Option<Catcher>,
catchers: HashMap<u16, Catcher>, catchers: HashMap<u16, Catcher>,
fairings: Fairings, fairings: Fairings,
shutdown_receiver: Option<mpsc::Receiver<()>>, shutdown_receiver: Option<mpsc::Receiver<()>>,
@ -91,14 +91,17 @@ impl Rocket {
fn _register(&mut self, catchers: Vec<Catcher>) { fn _register(&mut self, catchers: Vec<Catcher>) {
info!("{}{}", Paint::emoji("👾 "), Paint::magenta("Catchers:")); info!("{}{}", Paint::emoji("👾 "), Paint::magenta("Catchers:"));
for c in catchers { for catcher in catchers {
if self.catchers.get(&c.code).map_or(false, |e| !e.is_default) { info_!("{}", catcher);
info_!("{} {}", c, Paint::yellow("(warning: duplicate catcher!)"));
} else {
info_!("{}", c);
}
self.catchers.insert(c.code, c); let existing = match catcher.code {
Some(code) => self.catchers.insert(code, catcher),
None => self.default_catcher.replace(catcher)
};
if let Some(existing) = existing {
warn_!("Replacing existing '{}' catcher.", existing);
}
} }
} }
@ -120,7 +123,7 @@ impl Rocket {
manifest: vec![], manifest: vec![],
config: Config::development(), config: Config::development(),
router: Router::new(), router: Router::new(),
default_catchers: HashMap::new(), default_catcher: None,
catchers: HashMap::new(), catchers: HashMap::new(),
managed_state: Container::new(), managed_state: Container::new(),
fairings: Fairings::new(), fairings: Fairings::new(),
@ -459,19 +462,25 @@ impl Rocket {
req.cookies().reset_delta(); req.cookies().reset_delta();
// Try to get the active catcher but fallback to user's 500 catcher. // Try to get the active catcher but fallback to user's 500 catcher.
let catcher = self.catchers.get(&status.code).unwrap_or_else(|| { let code = Paint::red(status.code);
error_!("No catcher found for {}. Using 500 catcher.", status); let response = if let Some(catcher) = self.catchers.get(&status.code) {
self.catchers.get(&500).expect("500 catcher.") catcher.handler.handle(status, req).await
}); } else if let Some(ref default) = self.default_catcher {
warn_!("No {} catcher found. Using default catcher.", code);
default.handler.handle(status, req).await
} else {
warn_!("No {} or default catcher found. Using Rocket default catcher.", code);
crate::catcher::default(status, req)
};
// Dispatch to the user's catcher. If it fails, use the default 500. // Dispatch to the catcher. If it fails, use the Rocket default 500.
match catcher.handle(req).await { match response {
Ok(r) => return r, Ok(r) => r,
Err(err_status) => { Err(err_status) => {
error_!("Catcher failed with status: {}!", err_status); error_!("Catcher unexpectedly failed with {}.", err_status);
warn_!("Using default 500 error catcher."); warn_!("Using Rocket's default 500 error catcher.");
let default = self.default_catchers.get(&500).expect("Default 500"); let default = crate::catcher::default(Status::InternalServerError, req);
default.handle(req).await.expect("Default 500 response.") default.expect("Rocket has default 500 response")
} }
} }
} }
@ -660,8 +669,8 @@ impl Rocket {
shutdown_handle: Shutdown(shutdown_sender), shutdown_handle: Shutdown(shutdown_sender),
manifest: vec![], manifest: vec![],
router: Router::new(), router: Router::new(),
default_catchers: catcher::defaults::get(), default_catcher: None,
catchers: catcher::defaults::get(), catchers: HashMap::new(),
fairings: Fairings::new(), fairings: Fairings::new(),
shutdown_receiver: Some(shutdown_receiver), shutdown_receiver: Some(shutdown_receiver),
} }

View File

@ -141,7 +141,7 @@ impl Route {
/// ///
/// Panics if `path` is not a valid origin URI or Rocket route URI. /// Panics if `path` is not a valid origin URI or Rocket route URI.
pub fn new<S, H>(method: Method, path: S, handler: H) -> Route pub fn new<S, H>(method: Method, path: S, handler: H) -> Route
where S: AsRef<str>, H: Handler + 'static where S: AsRef<str>, H: Handler
{ {
let mut route = Route::ranked(0, method, path, handler); let mut route = Route::ranked(0, method, path, handler);
route.rank = default_rank(&route); route.rank = default_rank(&route);

View File

@ -2,30 +2,44 @@
#[cfg(test)] mod tests; #[cfg(test)] mod tests;
use rocket::response::content; use rocket::Request;
use rocket::response::{content, status};
use rocket::http::Status;
#[get("/hello/<name>/<age>")] #[get("/hello/<name>/<age>")]
fn hello(name: String, age: i8) -> String { fn hello(name: String, age: i8) -> String {
format!("Hello, {} year old named {}!", age, name) format!("Hello, {} year old named {}!", age, name)
} }
#[get("/<code>")]
fn forced_error(code: u16) -> Status {
Status::raw(code)
}
#[catch(404)] #[catch(404)]
fn not_found(req: &rocket::Request<'_>) -> content::Html<String> { fn not_found(req: &Request<'_>) -> content::Html<String> {
content::Html(format!("<p>Sorry, but '{}' is not a valid path!</p> content::Html(format!("<p>Sorry, but '{}' is not a valid path!</p>
<p>Try visiting /hello/&lt;name&gt;/&lt;age&gt; instead.</p>", <p>Try visiting /hello/&lt;name&gt;/&lt;age&gt; instead.</p>",
req.uri())) req.uri()))
} }
#[catch(default)]
fn default_catcher(status: Status, req: &Request<'_>) -> status::Custom<String> {
let msg = format!("{} - {} ({})", status.code, status.reason, req.uri());
status::Custom(status, msg)
}
fn rocket() -> rocket::Rocket {
rocket::ignite()
// .mount("/", routes![hello, hello]) // uncoment this to get an error
.mount("/", routes![hello, forced_error])
.register(catchers![not_found, default_catcher])
}
#[rocket::main] #[rocket::main]
async fn main() { async fn main() {
let result = rocket::ignite() if let Err(e) = rocket().launch().await {
// .mount("/", routes![hello, hello]) // uncoment this to get an error
.mount("/", routes![hello])
.register(catchers![not_found])
.launch().await;
if let Err(e) = result {
println!("Whoops! Rocket didn't launch!"); println!("Whoops! Rocket didn't launch!");
println!("This went wrong: {:?}", e); println!("Error: {:?}", e);
}; };
} }

View File

@ -1,31 +1,56 @@
use rocket::local::blocking::Client; use rocket::local::blocking::Client;
use rocket::http::Status; use rocket::http::Status;
fn test(uri: &str, status: Status, body: String) { #[test]
let rocket = rocket::ignite() fn test_hello() {
.mount("/", routes![super::hello]) let client = Client::new(super::rocket()).unwrap();
.register(catchers![super::not_found]);
let client = Client::new(rocket).unwrap(); let (name, age) = ("Arthur", 42);
let uri = format!("/hello/{}/{}", name, age);
let response = client.get(uri).dispatch(); let response = client.get(uri).dispatch();
assert_eq!(response.status(), status);
assert_eq!(response.into_string(), Some(body)); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string().unwrap(), super::hello(name.into(), age));
} }
#[test] #[test]
fn test_hello() { fn forced_error_and_default_catcher() {
let (name, age) = ("Arthur", 42); let client = Client::new(super::rocket()).unwrap();
let uri = format!("/hello/{}/{}", name, age);
test(&uri, Status::Ok, format!("Hello, {} year old named {}!", age, name)); let request = client.get("/404");
let expected = super::not_found(request.inner());
let response = request.dispatch();
assert_eq!(response.status(), Status::NotFound);
assert_eq!(response.into_string().unwrap(), expected.0);
let request = client.get("/405");
let expected = super::default_catcher(Status::MethodNotAllowed, request.inner());
let response = request.dispatch();
assert_eq!(response.status(), Status::MethodNotAllowed);
assert_eq!(response.into_string().unwrap(), expected.1);
let request = client.get("/533");
let expected = super::default_catcher(Status::raw(533), request.inner());
let response = request.dispatch();
assert_eq!(response.status(), Status::raw(533));
assert_eq!(response.into_string().unwrap(), expected.1);
let request = client.get("/700");
let expected = super::default_catcher(Status::InternalServerError, request.inner());
let response = request.dispatch();
assert_eq!(response.status(), Status::InternalServerError);
assert_eq!(response.into_string().unwrap(), expected.1);
} }
#[test] #[test]
fn test_hello_invalid_age() { fn test_hello_invalid_age() {
let client = Client::new(super::rocket()).unwrap();
for &(name, age) in &[("Ford", -129), ("Trillian", 128)] { for &(name, age) in &[("Ford", -129), ("Trillian", 128)] {
let uri = format!("/hello/{}/{}", name, age); let request = client.get(format!("/hello/{}/{}", name, age));
let body = format!("<p>Sorry, but '{}' is not a valid path!</p> let expected = super::not_found(request.inner());
<p>Try visiting /hello/&lt;name&gt;/&lt;age&gt; instead.</p>", let response = request.dispatch();
uri); assert_eq!(response.status(), Status::NotFound);
test(&uri, Status::NotFound, body); assert_eq!(response.into_string().unwrap(), expected.0);
} }
} }

View File

@ -3,12 +3,12 @@ mod tests;
use std::env; use std::env;
use rocket::{Request, Route, Data, Catcher, try_outcome}; use rocket::{Request, Route, Data};
use rocket::http::{Status, RawStr}; use rocket::http::{Status, RawStr, Method::*};
use rocket::response::{Responder, status::Custom}; use rocket::response::{Responder, status::Custom};
use rocket::handler::{Handler, Outcome, HandlerFuture, CatcherFuture}; use rocket::handler::{Handler, Outcome, HandlerFuture};
use rocket::outcome::IntoOutcome; use rocket::catcher::{Catcher, ErrorHandlerFuture};
use rocket::http::Method::*; use rocket::outcome::{try_outcome, IntoOutcome};
use rocket::tokio::fs::File; use rocket::tokio::fs::File;
fn forward<'r>(_req: &'r Request, data: Data) -> HandlerFuture<'r> { fn forward<'r>(_req: &'r Request, data: Data) -> HandlerFuture<'r> {
@ -64,7 +64,7 @@ fn get_upload<'r>(req: &'r Request, _: Data) -> HandlerFuture<'r> {
Outcome::from(req, std::fs::File::open(env::temp_dir().join("upload.txt")).ok()).pin() Outcome::from(req, std::fs::File::open(env::temp_dir().join("upload.txt")).ok()).pin()
} }
fn not_found_handler<'r>(req: &'r Request) -> CatcherFuture<'r> { fn not_found_handler<'r>(_: Status, req: &'r Request) -> ErrorHandlerFuture<'r> {
let res = Custom(Status::NotFound, format!("Couldn't find: {}", req.uri())); let res = Custom(Status::NotFound, format!("Couldn't find: {}", req.uri()));
Box::pin(async move { res.respond_to(req) }) Box::pin(async move { res.respond_to(req) })
} }

View File

@ -1088,27 +1088,26 @@ function, so we must `await` it.
## Error Catchers ## Error Catchers
Routing may fail for a variety of reasons. These include: Application processing is fallible. Errors arise from the following sources:
* A guard fails. * A failing guard.
* A handler returns a [`Responder`](../responses/#responder) that fails. * A failing responder.
* No routes matched. * A routing failure.
If any of these conditions occur, Rocket returns an error to the client. To do If any of these occur, Rocket returns an error to the client. To generate the
so, Rocket invokes the _catcher_ corresponding to the error's status code. error, Rocket invokes the _catcher_ corresponding to the error's status code.
Catchers are similar to routes except in that: Catchers are similar to routes except in that:
1. Catchers are only invoked on error conditions. 1. Catchers are only invoked on error conditions.
2. Catchers are declared with the `catch` attribute. 2. Catchers are declared with the `catch` attribute.
3. Catchers are _registered_ with [`register()`] instead of [`mount()`]. 3. Catchers are _registered_ with [`register()`] instead of [`mount()`].
4. Any modifications to cookies are cleared before a catcher is invoked. 4. Any modifications to cookies are cleared before a catcher is invoked.
5. Error catchers cannot invoke guards of any sort. 5. Error catchers cannot invoke guards.
6. Error catchers should not fail to produce a response.
Rocket provides default catchers for all of the standard HTTP error codes. To To declare a catcher for a given status code, use the [`catch`] attribute, which
override a default catcher, or declare a catcher for a custom status code, use takes a single integer corresponding to the HTTP status code to catch. For
the [`catch`] attribute, which takes a single integer corresponding to the HTTP instance, to declare a catcher for `404 Not Found` errors, you'd write:
status code to catch. For instance, to declare a catcher for `404 Not Found`
errors, you'd write:
```rust ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
@ -1120,8 +1119,10 @@ use rocket::Request;
fn not_found(req: &Request) { /* .. */ } fn not_found(req: &Request) { /* .. */ }
``` ```
As with routes, the return type (here `T`) must implement `Responder`. A Catchers may take zero, one, or two arguments. If the catcher takes one
concrete implementation may look like: argument, it must be of type [`&Request`]. It it takes two, they must be of type
[`Status`] and [`&Request`], in that order. As with routes, the return type must
implement `Responder`. A concrete implementation may look like:
```rust ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
@ -1152,12 +1153,38 @@ fn main() {
} }
``` ```
Unlike route request handlers, catchers take exactly zero or one parameter. If ### Default Catchers
the catcher takes a parameter, it must be of type [`&Request`]. The [error
catcher example](@example/errors) on GitHub illustrates their use in full. If no catcher for a given status code has been registered, Rocket calls the
_default_ catcher. Rocket provides a default catcher for all applications
automatically, so providing one is usually unnecessary. Rocket's built-in
default catcher can handle all errors. It produces HTML or JSON, depending on
the value of the `Accept` header. As such, a default catcher, or catchers in
general, only need to be registered if an error needs to be handled in a custom
fashion.
Declaring a default catcher is done with `#[catch(default)]`:
```rust
# #[macro_use] extern crate rocket;
# fn main() {}
use rocket::Request;
use rocket::http::Status;
#[catch(default)]
fn default_catcher(status: Status, request: &Request) { /* .. */ }
```
It must similarly be registered with [`register()`].
The [error catcher example](@example/errors) illustrates their use in full,
while the [`Catcher`] API documentation provides further details.
[`catch`]: @api/rocket/attr.catch.html [`catch`]: @api/rocket/attr.catch.html
[`register()`]: @api/rocket/struct.Rocket.html#method.register [`register()`]: @api/rocket/struct.Rocket.html#method.register
[`mount()`]: @api/rocket/struct.Rocket.html#method.mount [`mount()`]: @api/rocket/struct.Rocket.html#method.mount
[`catchers!`]: @api/rocket/macro.catchers.html [`catchers!`]: @api/rocket/macro.catchers.html
[`&Request`]: @api/rocket/struct.Request.html [`&Request`]: @api/rocket/struct.Request.html
[`Status`]: @api/rocket/http/struct.Status.html
[`Catcher`]: @api/rocket/catcher/struct.Catcher.html