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::{syn, MetaItem, Spanned, Result, FromMeta, Diagnostic};
use crate::http_codegen::{self, Optional};
use crate::proc_macro2::{TokenStream, Span};
use crate::http_codegen::Status;
use crate::syn_ext::{IdentExt, ReturnTypeExt, TokenStreamExt};
use self::syn::{Attribute, parse::Parser};
use crate::{CATCH_FN_PREFIX, CATCH_STRUCT_PREFIX};
@ -11,13 +11,35 @@ use crate::{CATCH_FN_PREFIX, CATCH_STRUCT_PREFIX};
#[derive(Debug, FromMeta)]
struct CatchAttribute {
#[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 {
/// 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.
function: syn::ItemFn,
}
@ -33,13 +55,14 @@ fn parse_params(
let full_attr = quote!(#[catch(#args)]);
let attrs = Attribute::parse_outer.parse2(full_attr)?;
let attribute = match CatchAttribute::from_attrs("catch", &attrs) {
Some(result) => result.map_err(|d| {
d.help("`#[catch]` expects a single status integer, e.g.: #[catch(404)]")
Some(result) => result.map_err(|diag| {
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"))
};
Ok(CatchParams { status: attribute.status, function })
Ok(CatchParams { status: attribute.status.0, function })
}
pub fn _catch(
@ -54,59 +77,41 @@ pub fn _catch(
let user_catcher_fn_name = catch.function.sig.ident.clone();
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 (vis, status) = (&catch.function.vis, &catch.status);
let status_code = status.0.code;
let (vis, catcher_status) = (&catch.function.vis, &catch.status);
let status_code = Optional(catcher_status.as_ref().map(|s| s.0.code));
// Variables names we'll use and reuse.
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.
if catch.function.sig.inputs.len() > 1 {
if catch.function.sig.inputs.len() > 2 {
return Err(catch.function.sig.paren_token.span
.error("invalid number of arguments: must be zero or one")
.help("catchers may optionally take an argument of type `&Request`"));
.error("invalid number of arguments: must be zero, one, or two")
.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.
let return_type_span = catch.function.sig.output.ty()
.map(|ty| ty.span().into())
.unwrap_or(Span::call_site().into());
// Set the `req` span to that of the arg for a correct `Wrong type` span.
let input = catch.function.sig.inputs.first()
.map(|arg| match arg {
syn::FnArg::Receiver(_) => req.respanned(arg.span()),
syn::FnArg::Typed(a) => req.respanned(a.ty.span())
});
// Set the `req` and `status` spans to that of their respective function
// arguments for a more correct `wrong type` error span. `rev` to be cute.
let codegen_args = &[&req, &status];
let inputs = catch.function.sig.inputs.iter().rev()
.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`.
let dot_await = catch.function.sig.asyncness
.map(|a| quote_spanned!(a.span().into() => .await));
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)?
});
@ -116,7 +121,10 @@ pub fn _catch(
/// Rocket code generated wrapping catch function.
#[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 {
let __response = #catcher_response;
#Response::build()
@ -129,8 +137,8 @@ pub fn _catch(
/// Rocket code generated static catcher info.
#[doc(hidden)]
#[allow(non_upper_case_globals)]
#vis static #generated_struct_name: ::rocket::StaticCatchInfo =
::rocket::StaticCatchInfo {
#vis static #generated_struct_name: ::rocket::StaticCatcherInfo =
::rocket::StaticCatcherInfo {
code: #status_code,
handler: #generated_fn_name,
};

View File

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

View File

@ -14,13 +14,13 @@ error: expected `fn`
|
= 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
|
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
--> $DIR/catch.rs:14:9
@ -28,7 +28,7 @@ error: unexpected keyed parameter: expected literal or identifier
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
--> $DIR/catch.rs:17:9
@ -36,7 +36,7 @@ error: unexpected keyed parameter: expected literal or identifier
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]
--> $DIR/catch.rs:20:9
@ -44,7 +44,7 @@ error: status must be in range [100, 599]
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]
--> $DIR/catch.rs:23:9
@ -52,7 +52,7 @@ error: status must be in range [100, 599]
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`
--> $DIR/catch.rs:26:14
@ -60,20 +60,16 @@ error: unexpected attribute parameter: `message`
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
--> $DIR/catch.rs:30:6
error[E0308]: mismatched types
--> $DIR/catch.rs:30:17
|
30 | fn f3(_request: &Request, other: bool) { }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: catchers may optionally take an argument of type `&Request`
| ^^^^^^^^ expected `&rocket::Request<'_>`, found struct `rocket::http::Status`
warning: unused import: `rocket::Request`
--> $DIR/catch.rs:3:5
error[E0308]: mismatched types
--> $DIR/catch.rs:30:34
|
3 | use rocket::Request;
| ^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
30 | fn f3(_request: &Request, other: bool) { }
| ^^^^ expected `bool`, found `&rocket::Request<'_>`

View File

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

View File

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

View File

@ -1,44 +1,67 @@
use std::future::Future;
use crate::response;
use crate::handler::ErrorHandler;
use crate::codegen::StaticCatchInfo;
use crate::request::Request;
//! Types and traits for error catchers, error handlers, and their return
//! values.
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.
///
/// Catchers are routes that run when errors occur. They correspond directly
/// 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.
/// # Overview
///
/// Because error handlers are only called when all routes are exhausted, they
/// should not fail nor forward. If an error catcher fails, the user will
/// receive no response. If an error catcher forwards, Rocket will respond with
/// an internal server error.
/// Catchers are routes that run when errors are produced by the application.
/// They consist of an [`ErrorHandler`] and an optional status code to match
/// against arising errors. Errors arise from the the following sources:
///
/// # Built-In Catchers
/// * A failing guard.
/// * A failing responder.
/// * Routing failure.
///
/// Rocket has many built-in, pre-registered default catchers. In particular,
/// Rocket has catchers for all of the following status codes: 400, 401, 402,
/// 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417,
/// 418, 421, 426, 428, 429, 431, 451, 500, 501, 503, and 510. As such, catchers
/// only need to be registered if an error needs to be handled in a custom
/// fashion.
/// Each failure is paired with a status code. Guards and responders indicate
/// the status code themselves via their `Err` return value while a routing
/// failure is always a `404`. Rocket invokes the error handler for the catcher
/// with the error's status code.
///
/// ## 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
///
/// Catchers should rarely be used directly. Instead, they are typically
/// declared using the `catch` decorator, as follows:
/// Catchers should rarely be constructed or used directly. Instead, they are
/// typically generated via the [`catch`] attribute, as follows:
///
/// ```rust,no_run
/// #[macro_use] extern crate rocket;
///
/// use rocket::Request;
/// use rocket::http::Status;
///
/// #[catch(500)]
/// fn internal_error() -> &'static str {
@ -50,74 +73,204 @@ use yansi::Color::*;
/// 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]
/// 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.
/// If the catcher takes an argument, it must be of type [`&Request`](Request).
/// A function decorated with `#[catch]` may take zero, one, or two arguments.
/// 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 {
/// The HTTP status code to match against.
pub code: u16,
/// The catcher's associated handler.
pub handler: ErrorHandler,
pub(crate) is_default: bool,
/// The HTTP status code to match against if this route is not `default`.
pub code: Option<u16>,
/// The catcher's associated error handler.
pub handler: Box<dyn ErrorHandler>,
}
impl Catcher {
/// Creates a catcher for the given status code using the given error
/// handler. This should only be used when routing manually.
/// Creates a catcher for the given status code, or a default catcher if
/// `code` is `None`, using the given error handler. This should only be
/// used when routing manually.
///
/// # Examples
///
/// ```rust
/// # #![allow(unused_variables)]
/// use rocket::{Catcher, Request};
/// use rocket::handler::CatcherFuture;
/// use rocket::response::{Result, Responder};
/// use rocket::response::status::Custom;
/// use rocket::request::Request;
/// use rocket::catcher::{Catcher, ErrorHandlerFuture};
/// use rocket::response::{Result, Responder, status::Custom};
/// use rocket::http::Status;
///
/// fn handle_404<'r>(req: &'r Request) -> CatcherFuture<'r> {
/// let res = Custom(Status::NotFound, format!("404: {}", req.uri()));
/// fn handle_404<'r>(status: Status, req: &'r Request<'_>) -> ErrorHandlerFuture<'r> {
/// let res = Custom(status, format!("404: {}", req.uri()));
/// 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) })
/// }
///
/// 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 internal_server_error_catcher = Catcher::new(500, handle_500);
/// let default_error_catcher = Catcher::new(None, handle_default);
/// ```
#[inline(always)]
pub fn new(code: u16, handler: ErrorHandler) -> Catcher {
Catcher { code, handler, is_default: false }
pub fn new<C, H>(code: C, handler: H) -> Catcher
where C: Into<Option<u16>>, H: ErrorHandler
{
Catcher { code: code.into(), handler: Box::new(handler) }
}
}
impl Default for Catcher {
fn default() -> Self {
fn async_default<'r>(status: Status, request: &'r Request<'_>) -> ErrorHandlerFuture<'r> {
Box::pin(async move { default(status, request) })
}
#[inline(always)]
pub(crate) fn handle<'r>(&self, req: &'r Request<'_>) -> impl Future<Output = response::Result<'r>> {
(self.handler)(req)
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)]
fn new_default(code: u16, handler: ErrorHandler) -> Catcher {
Catcher { code, handler, is_default: true, }
async fn handle<'r, 's: 'r>(&'s self, status: Status, req: &'r Request<'_>) -> Result<'r> {
self(status, req).await
}
}
#[doc(hidden)]
impl<'a> From<&'a StaticCatchInfo> for Catcher {
fn from(info: &'a StaticCatchInfo) -> Catcher {
impl<'a> From<&'a StaticCatcherInfo> for Catcher {
fn from(info: &'a StaticCatcherInfo) -> Catcher {
Catcher::new(info.code, info.handler)
}
}
impl fmt::Display for Catcher {
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 {
f.debug_struct("Catcher")
.field("code", &self.code)
.field("default", &self.is_default)
.finish()
}
}
macro_rules! error_page_template {
($code:expr, $name:expr, $description:expr) => (
macro_rules! html_error_template {
($code:expr, $reason:expr, $description:expr) => (
concat!(r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>"#, $code, " ", $name, r#"</title>
<title>"#, $code, " ", $reason, r#"</title>
</head>
<body align="center">
<div role="main" align="center">
<h1>"#, $code, ": ", $name, r#"</h1>
<h1>"#, $code, ": ", $reason, r#"</h1>
<p>"#, $description, r#"</p>
<hr />
</div>
@ -155,98 +307,136 @@ macro_rules! error_page_template {
)
}
macro_rules! default_catchers {
($($code:expr, $name:expr, $description:expr, $fn_name:ident),+) => (
let mut map = HashMap::new();
$(
fn $fn_name<'r>(req: &'r Request<'_>) -> crate::handler::CatcherFuture<'r> {
let status = Status::from_code($code).unwrap();
let html = content::Html(error_page_template!($code, $name, $description));
Box::pin(async move { status::Custom(status, html).respond_to(req) })
macro_rules! json_error_template {
($code:expr, $reason:expr, $description:expr) => (
concat!(
r#"{
"error": {
"code": "#, $code, r#",
"reason": ""#, $reason, r#"",
"description": ""#, $description, r#""
}
map.insert($code, Catcher::new_default($code, $fn_name));
)+
map
}"#
)
)
}
pub mod defaults {
use super::Catcher;
// This is unfortunate, but the `{`, `}` above make it unusable for `format!`.
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;
use crate::request::Request;
use crate::response::{content, status, Responder};
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};
pub fn get() -> HashMap<u16, Catcher> {
default_catchers! {
400, "Bad Request", "The request could not be understood by the server due
to malformed syntax.", handle_400,
401, "Unauthorized", "The request requires user authentication.",
handle_401,
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,
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
pub(crate) fn default<'r>(status: Status, req: &'r Request<'_>) -> Result<'r> {
if req.accept().map(|a| a.preferred().is_json()).unwrap_or(false) {
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()
};
status::Custom(status, content::Json(json)).respond_to(req)
} else {
let html: Cow<'_, str> = match status.code {
$($code => html_error_template!($code, $reason, $description).into(),)*
code => format!(html_error_template!("{}", "Unknown Error",
"An unknown error has occurred."), code, code).into(),
};
status::Custom(status, content::Html(html)).respond_to(req)
}
}
)
}
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::handler::{Outcome, ErrorHandler};
use crate::http::{Method, MediaType};
use crate::handler::HandlerFuture;
use crate::catcher::ErrorHandlerFuture;
use crate::http::{Status, Method, MediaType};
/// Type of a static handler, which users annotate with Rocket's attribute.
pub type StaticHandler = for<'r> fn(&'r Request<'_>, Data) -> BoxFuture<'r, Outcome<'r>>;
/// Type of a route handler, generated from a `fn` annotated with `#[route]`.
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.
pub struct StaticRouteInfo {
@ -24,9 +26,9 @@ pub struct StaticRouteInfo {
}
/// Information generated by the `catch` attribute during codegen.
pub struct StaticCatchInfo {
pub struct StaticCatcherInfo {
/// The catcher's status code.
pub code: u16,
pub code: Option<u16>,
/// 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 crate::data::Data;
use crate::request::Request;
use crate::response::{self, Response, Responder};
use crate::response::{Response, Responder};
use crate::http::Status;
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>;
}
/// 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]
impl<F: Clone + Sync + Send + 'static> Handler for F
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!
#[doc(hidden)]
pub fn dummy<'r>(r: &'r Request<'_>, _: Data) -> HandlerFuture<'r> {
@ -318,3 +288,29 @@ impl<'r, 'o: 'r> Outcome<'o> {
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 fairing;
pub mod error;
pub mod catcher;
// Reexport of HTTP everything.
pub mod http {
@ -122,16 +123,15 @@ mod shutdown;
mod router;
mod rocket;
mod codegen;
mod catcher;
mod ext;
#[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::config::Config;
#[doc(inline)] pub use crate::catcher::Catcher;
pub use crate::router::Route;
pub use crate::request::{Request, State};
pub use crate::catcher::Catcher;
pub use crate::rocket::{Cargo, Rocket};
pub use crate::shutdown::Shutdown;

View File

@ -42,8 +42,10 @@ impl<'c> LocalRequest<'c> {
method: Method,
uri: Cow<'c, str>
) -> LocalRequest<'c> {
// We set a dummy string for now and check the user's URI on dispatch.
let request = Request::new(client.rocket(), method, Origin::dummy());
// We try to validate the URI now so that the inner `Request` contains a
// 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.
if let Some(ref jar) = client.cookies {
@ -70,12 +72,11 @@ impl<'c> LocalRequest<'c> {
// Performs the actual dispatch.
async fn _dispatch(mut self) -> LocalResponse<'c> {
// First, validate the URI, returning an error response (generated from
// an error catcher) immediately if it's invalid.
// First, revalidate the URI, returning an error response (generated
// 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();
if let Ok(uri) = Origin::parse(&self.uri) {
self.request.set_uri(uri.into_owned());
} else {
if let Err(_) = Origin::parse(&self.uri) {
error!("Malformed request URI: {}", self.uri);
return LocalResponse::new(self.request, move |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> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
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
/// a warning message and returns an `Err` of `Status::NotFound`.
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);
warn_!("Fowarding to 500 (Internal Server Error) catcher.");
Err(Status::InternalServerError)
}
}

View File

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

View File

@ -2,30 +2,44 @@
#[cfg(test)] mod tests;
use rocket::response::content;
use rocket::Request;
use rocket::response::{content, status};
use rocket::http::Status;
#[get("/hello/<name>/<age>")]
fn hello(name: String, age: i8) -> String {
format!("Hello, {} year old named {}!", age, name)
}
#[get("/<code>")]
fn forced_error(code: u16) -> Status {
Status::raw(code)
}
#[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>
<p>Try visiting /hello/&lt;name&gt;/&lt;age&gt; instead.</p>",
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]
async fn main() {
let result = rocket::ignite()
// .mount("/", routes![hello, hello]) // uncoment this to get an error
.mount("/", routes![hello])
.register(catchers![not_found])
.launch().await;
if let Err(e) = result {
if let Err(e) = rocket().launch().await {
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::http::Status;
fn test(uri: &str, status: Status, body: String) {
let rocket = rocket::ignite()
.mount("/", routes![super::hello])
.register(catchers![super::not_found]);
#[test]
fn test_hello() {
let client = Client::new(super::rocket()).unwrap();
let client = Client::new(rocket).unwrap();
let (name, age) = ("Arthur", 42);
let uri = format!("/hello/{}/{}", name, age);
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]
fn test_hello() {
let (name, age) = ("Arthur", 42);
let uri = format!("/hello/{}/{}", name, age);
test(&uri, Status::Ok, format!("Hello, {} year old named {}!", age, name));
fn forced_error_and_default_catcher() {
let client = Client::new(super::rocket()).unwrap();
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]
fn test_hello_invalid_age() {
let client = Client::new(super::rocket()).unwrap();
for &(name, age) in &[("Ford", -129), ("Trillian", 128)] {
let uri = format!("/hello/{}/{}", name, age);
let body = format!("<p>Sorry, but '{}' is not a valid path!</p>
<p>Try visiting /hello/&lt;name&gt;/&lt;age&gt; instead.</p>",
uri);
test(&uri, Status::NotFound, body);
let request = client.get(format!("/hello/{}/{}", name, age));
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);
}
}

View File

@ -3,12 +3,12 @@ mod tests;
use std::env;
use rocket::{Request, Route, Data, Catcher, try_outcome};
use rocket::http::{Status, RawStr};
use rocket::{Request, Route, Data};
use rocket::http::{Status, RawStr, Method::*};
use rocket::response::{Responder, status::Custom};
use rocket::handler::{Handler, Outcome, HandlerFuture, CatcherFuture};
use rocket::outcome::IntoOutcome;
use rocket::http::Method::*;
use rocket::handler::{Handler, Outcome, HandlerFuture};
use rocket::catcher::{Catcher, ErrorHandlerFuture};
use rocket::outcome::{try_outcome, IntoOutcome};
use rocket::tokio::fs::File;
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()
}
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()));
Box::pin(async move { res.respond_to(req) })
}

View File

@ -1088,27 +1088,26 @@ function, so we must `await` it.
## 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 handler returns a [`Responder`](../responses/#responder) that fails.
* No routes matched.
* A failing guard.
* A failing responder.
* A routing failure.
If any of these conditions occur, Rocket returns an error to the client. To do
so, Rocket invokes the _catcher_ corresponding to the error's status code.
If any of these occur, Rocket returns an error to the client. To generate the
error, Rocket invokes the _catcher_ corresponding to the error's status code.
Catchers are similar to routes except in that:
1. Catchers are only invoked on error conditions.
2. Catchers are declared with the `catch` attribute.
3. Catchers are _registered_ with [`register()`] instead of [`mount()`].
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
override a default catcher, or declare a catcher for a custom status code, use
the [`catch`] attribute, which takes a single integer corresponding to the HTTP
status code to catch. For instance, to declare a catcher for `404 Not Found`
errors, you'd write:
To declare a catcher for a given status code, use the [`catch`] attribute, which
takes a single integer corresponding to the HTTP status code to catch. For
instance, to declare a catcher for `404 Not Found` errors, you'd write:
```rust
# #[macro_use] extern crate rocket;
@ -1120,8 +1119,10 @@ use rocket::Request;
fn not_found(req: &Request) { /* .. */ }
```
As with routes, the return type (here `T`) must implement `Responder`. A
concrete implementation may look like:
Catchers may take zero, one, or two arguments. If the catcher takes one
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
# #[macro_use] extern crate rocket;
@ -1152,12 +1153,38 @@ fn main() {
}
```
Unlike route request handlers, catchers take exactly zero or one parameter. If
the catcher takes a parameter, it must be of type [`&Request`]. The [error
catcher example](@example/errors) on GitHub illustrates their use in full.
### Default Catchers
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
[`register()`]: @api/rocket/struct.Rocket.html#method.register
[`mount()`]: @api/rocket/struct.Rocket.html#method.mount
[`catchers!`]: @api/rocket/macro.catchers.html
[`&Request`]: @api/rocket/struct.Request.html
[`Status`]: @api/rocket/http/struct.Status.html
[`Catcher`]: @api/rocket/catcher/struct.Catcher.html