rocket/catcher/
catcher.rs

1use std::fmt;
2use std::io::Cursor;
3
4use crate::http::uri::Path;
5use crate::http::ext::IntoOwned;
6use crate::response::Response;
7use crate::request::Request;
8use crate::http::{Status, ContentType, uri};
9use crate::catcher::{Handler, BoxFuture};
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;
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    /// use rocket::request::Request;
150    /// use rocket::catcher::{Catcher, BoxFuture};
151    /// use rocket::response::Responder;
152    /// use rocket::http::Status;
153    ///
154    /// fn handle_404<'r>(status: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
155    ///    let res = (status, format!("404: {}", req.uri()));
156    ///    Box::pin(async move { res.respond_to(req) })
157    /// }
158    ///
159    /// fn handle_500<'r>(_: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
160    ///     Box::pin(async move{ "Whoops, we messed up!".respond_to(req) })
161    /// }
162    ///
163    /// fn handle_default<'r>(status: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
164    ///    let res = (status, format!("{}: {}", status, req.uri()));
165    ///    Box::pin(async move { res.respond_to(req) })
166    /// }
167    ///
168    /// let not_found_catcher = Catcher::new(404, handle_404);
169    /// let internal_server_error_catcher = Catcher::new(500, handle_500);
170    /// let default_error_catcher = Catcher::new(None, handle_default);
171    /// ```
172    ///
173    /// # Panics
174    ///
175    /// Panics if `code` is not in the HTTP status code error range `[400,
176    /// 600)`.
177    #[inline(always)]
178    pub fn new<S, H>(code: S, handler: H) -> Catcher
179        where S: Into<Option<u16>>, H: Handler
180    {
181        let code = code.into();
182        if let Some(code) = code {
183            assert!(code >= 400 && code < 600);
184        }
185
186        Catcher {
187            name: None,
188            base: uri::Origin::root().clone(),
189            handler: Box::new(handler),
190            rank: rank(uri::Origin::root().path()),
191            code,
192            location: None,
193        }
194    }
195
196    /// Returns the mount point (base) of the catcher, which defaults to `/`.
197    ///
198    /// # Example
199    ///
200    /// ```rust
201    /// use rocket::request::Request;
202    /// use rocket::catcher::{Catcher, BoxFuture};
203    /// use rocket::response::Responder;
204    /// use rocket::http::Status;
205    ///
206    /// fn handle_404<'r>(status: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
207    ///    let res = (status, format!("404: {}", req.uri()));
208    ///    Box::pin(async move { res.respond_to(req) })
209    /// }
210    ///
211    /// let catcher = Catcher::new(404, handle_404);
212    /// assert_eq!(catcher.base(), "/");
213    ///
214    /// let catcher = catcher.map_base(|base| format!("/foo/bar/{}", base)).unwrap();
215    /// assert_eq!(catcher.base(), "/foo/bar");
216    /// ```
217    pub fn base(&self) -> Path<'_> {
218        self.base.path()
219    }
220
221    /// Prefix `base` to the current `base` in `self.`
222    ///
223    /// If the the current base is `/`, then the base is replaced by `base`.
224    /// Otherwise, `base` is prefixed to the existing `base`.
225    ///
226    /// ```rust
227    /// use rocket::request::Request;
228    /// use rocket::catcher::{Catcher, BoxFuture};
229    /// use rocket::response::Responder;
230    /// use rocket::http::Status;
231    /// # use rocket::uri;
232    ///
233    /// fn handle_404<'r>(status: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
234    ///    let res = (status, format!("404: {}", req.uri()));
235    ///    Box::pin(async move { res.respond_to(req) })
236    /// }
237    ///
238    /// let catcher = Catcher::new(404, handle_404);
239    /// assert_eq!(catcher.base(), "/");
240    ///
241    /// // Since the base is `/`, rebasing replaces the base.
242    /// let rebased = catcher.rebase(uri!("/boo"));
243    /// assert_eq!(rebased.base(), "/boo");
244    ///
245    /// // Now every rebase prefixes.
246    /// let rebased = rebased.rebase(uri!("/base"));
247    /// assert_eq!(rebased.base(), "/base/boo");
248    ///
249    /// // Note that trailing slashes have no effect and are thus removed:
250    /// let catcher = Catcher::new(404, handle_404);
251    /// let rebased = catcher.rebase(uri!("/boo/"));
252    /// assert_eq!(rebased.base(), "/boo");
253    /// ```
254    pub fn rebase(mut self, mut base: uri::Origin<'_>) -> Self {
255        self.base = if self.base.path() == "/" {
256            base.clear_query();
257            base.into_normalized_nontrailing().into_owned()
258        } else {
259            uri::Origin::parse_owned(format!("{}{}", base.path(), self.base))
260                .expect("catcher rebase: {new}{old} is valid origin URI")
261                .into_normalized_nontrailing()
262        };
263
264        self.rank = rank(self.base());
265        self
266    }
267
268    /// Maps the `base` of this catcher using `mapper`, returning a new
269    /// `Catcher` with the returned base.
270    ///
271    /// **Note:** Prefer to use [`Catcher::rebase()`] whenever possible!
272    ///
273    /// `mapper` is called with the current base. The returned `String` is used
274    /// as the new base if it is a valid URI. If the returned base URI contains
275    /// a query, it is ignored. Returns an error if the base produced by
276    /// `mapper` is not a valid origin URI.
277    ///
278    /// # Example
279    ///
280    /// ```rust
281    /// use rocket::request::Request;
282    /// use rocket::catcher::{Catcher, BoxFuture};
283    /// use rocket::response::Responder;
284    /// use rocket::http::Status;
285    ///
286    /// fn handle_404<'r>(status: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
287    ///    let res = (status, format!("404: {}", req.uri()));
288    ///    Box::pin(async move { res.respond_to(req) })
289    /// }
290    ///
291    /// let catcher = Catcher::new(404, handle_404);
292    /// assert_eq!(catcher.base(), "/");
293    ///
294    /// let catcher = catcher.map_base(|_| format!("/bar")).unwrap();
295    /// assert_eq!(catcher.base(), "/bar");
296    ///
297    /// let catcher = catcher.map_base(|base| format!("/foo{}", base)).unwrap();
298    /// assert_eq!(catcher.base(), "/foo/bar");
299    ///
300    /// let catcher = catcher.map_base(|base| format!("/foo ? {}", base));
301    /// assert!(catcher.is_err());
302    /// ```
303    pub fn map_base<'a, F>(mut self, mapper: F) -> Result<Self, uri::Error<'static>>
304        where F: FnOnce(uri::Origin<'a>) -> String
305    {
306        let new_base = uri::Origin::parse_owned(mapper(self.base))?;
307        self.base = new_base.into_normalized_nontrailing();
308        self.base.clear_query();
309        self.rank = rank(self.base());
310        Ok(self)
311    }
312}
313
314impl Default for Catcher {
315    fn default() -> Self {
316        fn handler<'r>(s: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
317            Box::pin(async move { Ok(default_handler(s, req)) })
318        }
319
320        let mut catcher = Catcher::new(None, handler);
321        catcher.name = Some("<Rocket Catcher>".into());
322        catcher
323    }
324}
325
326/// Information generated by the `catch` attribute during codegen.
327#[doc(hidden)]
328pub struct StaticInfo {
329    /// The catcher's name, i.e, the name of the function.
330    pub name: &'static str,
331    /// The catcher's status code.
332    pub code: Option<u16>,
333    /// The catcher's handler, i.e, the annotated function.
334    pub handler: for<'r> fn(Status, &'r Request<'_>) -> BoxFuture<'r>,
335    /// The file, line, and column where the catcher was defined.
336    pub location: (&'static str, u32, u32),
337}
338
339#[doc(hidden)]
340impl From<StaticInfo> for Catcher {
341    #[inline]
342    fn from(info: StaticInfo) -> Catcher {
343        let mut catcher = Catcher::new(info.code, info.handler);
344        catcher.name = Some(info.name.into());
345        catcher.location = Some(info.location);
346        catcher
347    }
348}
349
350impl fmt::Debug for Catcher {
351    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352        f.debug_struct("Catcher")
353            .field("name", &self.name)
354            .field("base", &self.base)
355            .field("code", &self.code)
356            .field("rank", &self.rank)
357            .finish()
358    }
359}
360
361macro_rules! html_error_template {
362    ($code:expr, $reason:expr, $description:expr) => (
363        concat!(
364r#"<!DOCTYPE html>
365<html lang="en">
366<head>
367    <meta charset="utf-8">
368    <meta name="color-scheme" content="light dark">
369    <title>"#, $code, " ", $reason, r#"</title>
370</head>
371<body align="center">
372    <div role="main" align="center">
373        <h1>"#, $code, ": ", $reason, r#"</h1>
374        <p>"#, $description, r#"</p>
375        <hr />
376    </div>
377    <div role="contentinfo" align="center">
378        <small>Rocket</small>
379    </div>
380</body>
381</html>"#
382        )
383    )
384}
385
386macro_rules! json_error_template {
387    ($code:expr, $reason:expr, $description:expr) => (
388        concat!(
389r#"{
390  "error": {
391    "code": "#, $code, r#",
392    "reason": ""#, $reason, r#"",
393    "description": ""#, $description, r#""
394  }
395}"#
396        )
397    )
398}
399
400// This is unfortunate, but the `{`, `}` above make it unusable for `format!`.
401macro_rules! json_error_fmt_template {
402    ($code:expr, $reason:expr, $description:expr) => (
403        concat!(
404r#"{{
405  "error": {{
406    "code": "#, $code, r#",
407    "reason": ""#, $reason, r#"",
408    "description": ""#, $description, r#""
409  }}
410}}"#
411        )
412    )
413}
414
415macro_rules! default_handler_fn {
416    ($($code:expr, $reason:expr, $description:expr),+) => (
417        use std::borrow::Cow;
418
419        pub(crate) fn default_handler<'r>(
420            status: Status,
421            req: &'r Request<'_>
422        ) -> Response<'r> {
423            let preferred = req.accept().map(|a| a.preferred());
424            let (mime, text) = if preferred.map_or(false, |a| a.is_json()) {
425                let json: Cow<'_, str> = match status.code {
426                    $($code => json_error_template!($code, $reason, $description).into(),)*
427                    code => format!(json_error_fmt_template!("{}", "Unknown Error",
428                            "An unknown error has occurred."), code).into()
429                };
430
431                (ContentType::JSON, json)
432            } else {
433                let html: Cow<'_, str> = match status.code {
434                    $($code => html_error_template!($code, $reason, $description).into(),)*
435                    code => format!(html_error_template!("{}", "Unknown Error",
436                            "An unknown error has occurred."), code, code).into(),
437                };
438
439                (ContentType::HTML, html)
440            };
441
442            let mut r = Response::build().status(status).header(mime).finalize();
443            match text {
444                Cow::Owned(v) => r.set_sized_body(v.len(), Cursor::new(v)),
445                Cow::Borrowed(v) => r.set_sized_body(v.len(), Cursor::new(v)),
446            };
447
448            r
449        }
450    )
451}
452
453default_handler_fn! {
454    400, "Bad Request", "The request could not be understood by the server due \
455        to malformed syntax.",
456    401, "Unauthorized", "The request requires user authentication.",
457    402, "Payment Required", "The request could not be processed due to lack of payment.",
458    403, "Forbidden", "The server refused to authorize the request.",
459    404, "Not Found", "The requested resource could not be found.",
460    405, "Method Not Allowed", "The request method is not supported for the requested resource.",
461    406, "Not Acceptable", "The requested resource is capable of generating only content not \
462        acceptable according to the Accept headers sent in the request.",
463    407, "Proxy Authentication Required", "Authentication with the proxy is required.",
464    408, "Request Timeout", "The server timed out waiting for the request.",
465    409, "Conflict", "The request could not be processed because of a conflict in the request.",
466    410, "Gone", "The resource requested is no longer available and will not be available again.",
467    411, "Length Required", "The request did not specify the length of its content, which is \
468        required by the requested resource.",
469    412, "Precondition Failed", "The server does not meet one of the \
470        preconditions specified in the request.",
471    413, "Payload Too Large", "The request is larger than the server is \
472        willing or able to process.",
473    414, "URI Too Long", "The URI provided was too long for the server to process.",
474    415, "Unsupported Media Type", "The request entity has a media type which \
475        the server or resource does not support.",
476    416, "Range Not Satisfiable", "The portion of the requested file cannot be \
477        supplied by the server.",
478    417, "Expectation Failed", "The server cannot meet the requirements of the \
479        Expect request-header field.",
480    418, "I'm a teapot", "I was requested to brew coffee, and I am a teapot.",
481    421, "Misdirected Request", "The server cannot produce a response for this request.",
482    422, "Unprocessable Entity", "The request was well-formed but was unable to \
483        be followed due to semantic errors.",
484    426, "Upgrade Required", "Switching to the protocol in the Upgrade header field is required.",
485    428, "Precondition Required", "The server requires the request to be conditional.",
486    429, "Too Many Requests", "Too many requests have been received recently.",
487    431, "Request Header Fields Too Large", "The server is unwilling to process \
488        the request because either an individual header field, or all the header \
489        fields collectively, are too large.",
490    451, "Unavailable For Legal Reasons", "The requested resource is unavailable \
491        due to a legal demand to deny access to this resource.",
492    500, "Internal Server Error", "The server encountered an internal error while \
493        processing this request.",
494    501, "Not Implemented", "The server either does not recognize the request \
495        method, or it lacks the ability to fulfill the request.",
496    503, "Service Unavailable", "The server is currently unavailable.",
497    504, "Gateway Timeout", "The server did not receive a timely response from an upstream server.",
498    510, "Not Extended", "Further extensions to the request are required for \
499        the server to fulfill it."
500}