rocket_community/catcher/
catcher.rs

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