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}