rocket/catcher/
catcher.rs

1use std::fmt;
2use std::io::Cursor;
3
4use crate::response::Response;
5use crate::request::Request;
6use crate::http::{Status, ContentType, uri};
7use crate::catcher::{Handler, BoxFuture};
8
9use yansi::Paint;
10
11/// An error catching route.
12///
13/// Catchers are routes that run when errors are produced by the application.
14/// They consist of a [`Handler`] and an optional status code to match against
15/// arising errors. Errors arise from the the following sources:
16///
17///   * A failing guard.
18///   * A failing responder.
19///   * A forwarding guard.
20///   * Routing failure.
21///
22/// Each error or forward is paired with a status code. Guards and responders
23/// indicate the status code themselves via their `Err` and `Outcome` return
24/// value. A complete routing failure is always a `404`. Rocket invokes the
25/// error handler for the catcher with an error's status code, or in the case of
26/// every route resulting in a forward, the last forwarded status code.
27///
28/// ### Error Handler Restrictions
29///
30/// Because error handlers are a last resort, they should not fail to produce a
31/// response. If an error handler _does_ fail, Rocket invokes its default `500`
32/// error catcher. Error handlers cannot forward.
33///
34/// # Routing
35///
36/// If a route fails by returning an error [`Outcome`], Rocket routes the
37/// erroring request to the highest precedence catcher among all the catchers
38/// that match. Precedence is determined by the catcher's _base_, which is
39/// provided as the first argument to [`Rocket::register()`]. Catchers with more
40/// non-empty segments have a higher precedence.
41///
42/// Rocket provides [built-in defaults](#built-in-default), but _default_
43/// catchers can also be registered. A _default_ catcher is a catcher with no
44/// explicit status code: `None`.
45///
46/// [`Outcome`]: crate::request::Outcome
47/// [`Rocket::register()`]: crate::Rocket::register()
48///
49/// # Collisions
50///
51/// Two catchers are said to _collide_ if there exists an error that matches
52/// both catchers. Colliding catchers present a routing ambiguity and are thus
53/// disallowed by Rocket. Because catchers can be constructed dynamically,
54/// collision checking is done at [`ignite`](crate::Rocket::ignite()) time,
55/// after it becomes statically impossible to register any more catchers on an
56/// instance of `Rocket`.
57///
58/// ### Built-In Default
59///
60/// Rocket's provides a built-in default catcher that can handle all errors. It
61/// produces HTML or JSON, depending on the value of the `Accept` header. As
62/// such, catchers only need to be registered if an error needs to be handled in
63/// a custom fashion. The built-in default never conflicts with any
64/// user-registered catchers.
65///
66/// # Code Generation
67///
68/// Catchers should rarely be constructed or used directly. Instead, they are
69/// typically generated via the [`catch`] attribute, as follows:
70///
71/// ```rust,no_run
72/// #[macro_use] extern crate rocket;
73///
74/// use rocket::Request;
75/// use rocket::http::Status;
76///
77/// #[catch(500)]
78/// fn internal_error() -> &'static str {
79///     "Whoops! Looks like we messed up."
80/// }
81///
82/// #[catch(404)]
83/// fn not_found(req: &Request) -> String {
84///     format!("I couldn't find '{}'. Try something else?", req.uri())
85/// }
86///
87/// #[catch(default)]
88/// fn default(status: Status, req: &Request) -> String {
89///     format!("{} ({})", status, req.uri())
90/// }
91///
92/// #[launch]
93/// fn rocket() -> _ {
94///     rocket::build().register("/", catchers![internal_error, not_found, default])
95/// }
96/// ```
97///
98/// A function decorated with `#[catch]` may take zero, one, or two arguments.
99/// It's type signature must be one of the following, where `R:`[`Responder`]:
100///
101///   * `fn() -> R`
102///   * `fn(`[`&Request`]`) -> R`
103///   * `fn(`[`Status`]`, `[`&Request`]`) -> R`
104///
105/// See the [`catch`] documentation for full details.
106///
107/// [`catch`]: crate::catch
108/// [`Responder`]: crate::response::Responder
109/// [`&Request`]: crate::request::Request
110/// [`Status`]: crate::http::Status
111#[derive(Clone)]
112pub struct Catcher {
113    /// The name of this catcher, if one was given.
114    pub name: Option<Cow<'static, str>>,
115
116    /// The mount point.
117    pub base: uri::Origin<'static>,
118
119    /// The HTTP status to match against if this route is not `default`.
120    pub code: Option<u16>,
121
122    /// The catcher's associated error handler.
123    pub handler: Box<dyn Handler>,
124}
125
126impl Catcher {
127    /// Creates a catcher for the given `status`, or a default catcher if
128    /// `status` is `None`, using the given error handler. This should only be
129    /// used when routing manually.
130    ///
131    /// # Examples
132    ///
133    /// ```rust
134    /// use rocket::request::Request;
135    /// use rocket::catcher::{Catcher, BoxFuture};
136    /// use rocket::response::Responder;
137    /// use rocket::http::Status;
138    ///
139    /// fn handle_404<'r>(status: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
140    ///    let res = (status, format!("404: {}", req.uri()));
141    ///    Box::pin(async move { res.respond_to(req) })
142    /// }
143    ///
144    /// fn handle_500<'r>(_: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
145    ///     Box::pin(async move{ "Whoops, we messed up!".respond_to(req) })
146    /// }
147    ///
148    /// fn handle_default<'r>(status: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
149    ///    let res = (status, format!("{}: {}", status, req.uri()));
150    ///    Box::pin(async move { res.respond_to(req) })
151    /// }
152    ///
153    /// let not_found_catcher = Catcher::new(404, handle_404);
154    /// let internal_server_error_catcher = Catcher::new(500, handle_500);
155    /// let default_error_catcher = Catcher::new(None, handle_default);
156    /// ```
157    ///
158    /// # Panics
159    ///
160    /// Panics if `code` is not in the HTTP status code error range `[400,
161    /// 600)`.
162    #[inline(always)]
163    pub fn new<S, H>(code: S, handler: H) -> Catcher
164        where S: Into<Option<u16>>, H: Handler
165    {
166        let code = code.into();
167        if let Some(code) = code {
168            assert!(code >= 400 && code < 600);
169        }
170
171        Catcher {
172            name: None,
173            base: uri::Origin::ROOT,
174            handler: Box::new(handler),
175            code,
176        }
177    }
178
179    /// Maps the `base` of this catcher using `mapper`, returning a new
180    /// `Catcher` with the returned base.
181    ///
182    /// `mapper` is called with the current base. The returned `String` is used
183    /// as the new base if it is a valid URI. If the returned base URI contains
184    /// a query, it is ignored. Returns an error if the base produced by
185    /// `mapper` is not a valid origin URI.
186    ///
187    /// # Example
188    ///
189    /// ```rust
190    /// use rocket::request::Request;
191    /// use rocket::catcher::{Catcher, BoxFuture};
192    /// use rocket::response::Responder;
193    /// use rocket::http::Status;
194    ///
195    /// fn handle_404<'r>(status: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
196    ///    let res = (status, format!("404: {}", req.uri()));
197    ///    Box::pin(async move { res.respond_to(req) })
198    /// }
199    ///
200    /// let catcher = Catcher::new(404, handle_404);
201    /// assert_eq!(catcher.base.path(), "/");
202    ///
203    /// let catcher = catcher.map_base(|_| format!("/bar")).unwrap();
204    /// assert_eq!(catcher.base.path(), "/bar");
205    ///
206    /// let catcher = catcher.map_base(|base| format!("/foo{}", base)).unwrap();
207    /// assert_eq!(catcher.base.path(), "/foo/bar");
208    ///
209    /// let catcher = catcher.map_base(|base| format!("/foo ? {}", base));
210    /// assert!(catcher.is_err());
211    /// ```
212    pub fn map_base<'a, F>(
213        mut self,
214        mapper: F
215    ) -> std::result::Result<Self, uri::Error<'static>>
216        where F: FnOnce(uri::Origin<'a>) -> String
217    {
218        self.base = uri::Origin::parse_owned(mapper(self.base))?.into_normalized();
219        self.base.clear_query();
220        Ok(self)
221    }
222}
223
224impl Default for Catcher {
225    fn default() -> Self {
226        fn handler<'r>(s: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
227            Box::pin(async move { Ok(default_handler(s, req)) })
228        }
229
230        let mut catcher = Catcher::new(None, handler);
231        catcher.name = Some("<Rocket Catcher>".into());
232        catcher
233    }
234}
235
236/// Information generated by the `catch` attribute during codegen.
237#[doc(hidden)]
238pub struct StaticInfo {
239    /// The catcher's name, i.e, the name of the function.
240    pub name: &'static str,
241    /// The catcher's status code.
242    pub code: Option<u16>,
243    /// The catcher's handler, i.e, the annotated function.
244    pub handler: for<'r> fn(Status, &'r Request<'_>) -> BoxFuture<'r>,
245}
246
247#[doc(hidden)]
248impl From<StaticInfo> for Catcher {
249    #[inline]
250    fn from(info: StaticInfo) -> Catcher {
251        let mut catcher = Catcher::new(info.code, info.handler);
252        catcher.name = Some(info.name.into());
253        catcher
254    }
255}
256
257impl fmt::Display for Catcher {
258    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259        if let Some(ref n) = self.name {
260            write!(f, "{}{}{} ", "(".cyan(), n.primary(), ")".cyan())?;
261        }
262
263        if self.base.path() != "/" {
264            write!(f, "{} ", self.base.path().green())?;
265        }
266
267        match self.code {
268            Some(code) => write!(f, "{}", code.blue()),
269            None => write!(f, "{}", "default".blue()),
270        }
271    }
272}
273
274impl fmt::Debug for Catcher {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        f.debug_struct("Catcher")
277            .field("name", &self.name)
278            .field("base", &self.base)
279            .field("code", &self.code)
280            .finish()
281    }
282}
283
284macro_rules! html_error_template {
285    ($code:expr, $reason:expr, $description:expr) => (
286        concat!(
287r#"<!DOCTYPE html>
288<html lang="en">
289<head>
290    <meta charset="utf-8">
291    <meta name="color-scheme" content="light dark">
292    <title>"#, $code, " ", $reason, r#"</title>
293</head>
294<body align="center">
295    <div role="main" align="center">
296        <h1>"#, $code, ": ", $reason, r#"</h1>
297        <p>"#, $description, r#"</p>
298        <hr />
299    </div>
300    <div role="contentinfo" align="center">
301        <small>Rocket</small>
302    </div>
303</body>
304</html>"#
305        )
306    )
307}
308
309macro_rules! json_error_template {
310    ($code:expr, $reason:expr, $description:expr) => (
311        concat!(
312r#"{
313  "error": {
314    "code": "#, $code, r#",
315    "reason": ""#, $reason, r#"",
316    "description": ""#, $description, r#""
317  }
318}"#
319        )
320    )
321}
322
323// This is unfortunate, but the `{`, `}` above make it unusable for `format!`.
324macro_rules! json_error_fmt_template {
325    ($code:expr, $reason:expr, $description:expr) => (
326        concat!(
327r#"{{
328  "error": {{
329    "code": "#, $code, r#",
330    "reason": ""#, $reason, r#"",
331    "description": ""#, $description, r#""
332  }}
333}}"#
334        )
335    )
336}
337
338macro_rules! default_handler_fn {
339    ($($code:expr, $reason:expr, $description:expr),+) => (
340        use std::borrow::Cow;
341
342        pub(crate) fn default_handler<'r>(
343            status: Status,
344            req: &'r Request<'_>
345        ) -> Response<'r> {
346            let preferred = req.accept().map(|a| a.preferred());
347            let (mime, text) = if preferred.map_or(false, |a| a.is_json()) {
348                let json: Cow<'_, str> = match status.code {
349                    $($code => json_error_template!($code, $reason, $description).into(),)*
350                    code => format!(json_error_fmt_template!("{}", "Unknown Error",
351                            "An unknown error has occurred."), code).into()
352                };
353
354                (ContentType::JSON, json)
355            } else {
356                let html: Cow<'_, str> = match status.code {
357                    $($code => html_error_template!($code, $reason, $description).into(),)*
358                    code => format!(html_error_template!("{}", "Unknown Error",
359                            "An unknown error has occurred."), code, code).into(),
360                };
361
362                (ContentType::HTML, html)
363            };
364
365            let mut r = Response::build().status(status).header(mime).finalize();
366            match text {
367                Cow::Owned(v) => r.set_sized_body(v.len(), Cursor::new(v)),
368                Cow::Borrowed(v) => r.set_sized_body(v.len(), Cursor::new(v)),
369            };
370
371            r
372        }
373    )
374}
375
376default_handler_fn! {
377    400, "Bad Request", "The request could not be understood by the server due \
378        to malformed syntax.",
379    401, "Unauthorized", "The request requires user authentication.",
380    402, "Payment Required", "The request could not be processed due to lack of payment.",
381    403, "Forbidden", "The server refused to authorize the request.",
382    404, "Not Found", "The requested resource could not be found.",
383    405, "Method Not Allowed", "The request method is not supported for the requested resource.",
384    406, "Not Acceptable", "The requested resource is capable of generating only content not \
385        acceptable according to the Accept headers sent in the request.",
386    407, "Proxy Authentication Required", "Authentication with the proxy is required.",
387    408, "Request Timeout", "The server timed out waiting for the request.",
388    409, "Conflict", "The request could not be processed because of a conflict in the request.",
389    410, "Gone", "The resource requested is no longer available and will not be available again.",
390    411, "Length Required", "The request did not specify the length of its content, which is \
391        required by the requested resource.",
392    412, "Precondition Failed", "The server does not meet one of the \
393        preconditions specified in the request.",
394    413, "Payload Too Large", "The request is larger than the server is \
395        willing or able to process.",
396    414, "URI Too Long", "The URI provided was too long for the server to process.",
397    415, "Unsupported Media Type", "The request entity has a media type which \
398        the server or resource does not support.",
399    416, "Range Not Satisfiable", "The portion of the requested file cannot be \
400        supplied by the server.",
401    417, "Expectation Failed", "The server cannot meet the requirements of the \
402        Expect request-header field.",
403    418, "I'm a teapot", "I was requested to brew coffee, and I am a teapot.",
404    421, "Misdirected Request", "The server cannot produce a response for this request.",
405    422, "Unprocessable Entity", "The request was well-formed but was unable to \
406        be followed due to semantic errors.",
407    426, "Upgrade Required", "Switching to the protocol in the Upgrade header field is required.",
408    428, "Precondition Required", "The server requires the request to be conditional.",
409    429, "Too Many Requests", "Too many requests have been received recently.",
410    431, "Request Header Fields Too Large", "The server is unwilling to process \
411        the request because either an individual header field, or all the header \
412        fields collectively, are too large.",
413    451, "Unavailable For Legal Reasons", "The requested resource is unavailable \
414        due to a legal demand to deny access to this resource.",
415    500, "Internal Server Error", "The server encountered an internal error while \
416        processing this request.",
417    501, "Not Implemented", "The server either does not recognize the request \
418        method, or it lacks the ability to fulfill the request.",
419    503, "Service Unavailable", "The server is currently unavailable.",
420    504, "Gateway Timeout", "The server did not receive a timely response from an upstream server.",
421    510, "Not Extended", "Further extensions to the request are required for \
422        the server to fulfill it."
423}