mirror of
https://github.com/rwf2/Rocket.git
synced 2025-01-17 23:19:06 +00:00
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:
parent
e531770989
commit
45b4436ed3
@ -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,
|
||||
};
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
|
||||
3 | use rocket::Request;
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: `#[warn(unused_imports)]` on by default
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/catch.rs:30:34
|
||||
|
|
||||
30 | fn f3(_request: &Request, other: bool) { }
|
||||
| ^^^^ expected `bool`, found `&rocket::Request<'_>`
|
||||
|
@ -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
|
||||
|
|
||||
3 | use rocket::Request;
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: `#[warn(unused_imports)]` on by default
|
||||
error[E0308]: mismatched types
|
||||
--> $DIR/catch.rs:30:34
|
||||
|
|
||||
30 | fn f3(_request: &Request, other: bool) { }
|
||||
| ^^^^ expected `bool`, found `&rocket::Request<'_>`
|
||||
|
@ -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,
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn handle<'r>(&self, req: &'r Request<'_>) -> impl Future<Output = response::Result<'r>> {
|
||||
(self.handler)(req)
|
||||
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) })
|
||||
}
|
||||
|
||||
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) })
|
||||
}
|
||||
|
||||
map.insert($code, Catcher::new_default($code, $fn_name));
|
||||
)+
|
||||
|
||||
map
|
||||
macro_rules! json_error_template {
|
||||
($code:expr, $reason:expr, $description:expr) => (
|
||||
concat!(
|
||||
r#"{
|
||||
"error": {
|
||||
"code": "#, $code, r#",
|
||||
"reason": ""#, $reason, r#"",
|
||||
"description": ""#, $description, r#""
|
||||
}
|
||||
}"#
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
use crate::response::{content, status, Responder};
|
||||
use crate::http::Status;
|
||||
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()
|
||||
};
|
||||
|
||||
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
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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/<name>/<age> 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);
|
||||
};
|
||||
}
|
||||
|
@ -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/<name>/<age> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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) })
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user