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}