via/error.rs
1//! Conviently work with errors that may occur in an application.
2//!
3
4use http::header::CONTENT_TYPE;
5use http::StatusCode;
6use serde::ser::SerializeStruct;
7use serde::{Serialize, Serializer};
8use std::error::Error as StdError;
9use std::fmt::{self, Debug, Display, Formatter};
10use std::io;
11
12use crate::response::Response;
13
14/// A type alias for a boxed
15/// [`Error`](std::error::Error)
16/// that is `Send + Sync`.
17///
18pub type DynError = Box<dyn std::error::Error + Send + Sync>;
19
20/// An error type that can act as a specialized version of a
21/// [`ResponseBuilder`](crate::response::ResponseBuilder).
22///
23#[derive(Debug)]
24pub struct Error {
25 as_json: bool,
26 status: StatusCode,
27 message: Option<String>,
28 error: DynError,
29}
30
31/// A serialized representation of an individual error.
32///
33struct SerializeError<'a> {
34 message: &'a str,
35}
36
37impl Error {
38 /// Create a new [`Error`] from the provided source.
39 ///
40 pub fn new(source: DynError) -> Self {
41 Self::internal_server_error(source)
42 }
43
44 /// Create a new [`Error`] from the provided [`io::Error`]. The status code
45 /// of the error returned will correspond to `source.kind()`.
46 ///
47 pub fn from_io(source: io::Error) -> Self {
48 match source.kind() {
49 io::ErrorKind::AlreadyExists => {
50 // Implies a resource already exists.
51 Self::conflict(Box::new(source))
52 }
53
54 io::ErrorKind::BrokenPipe
55 | io::ErrorKind::ConnectionReset
56 | io::ErrorKind::ConnectionAborted => {
57 // Signals a broken connection.
58 Self::bad_gateway(Box::new(source))
59 }
60
61 io::ErrorKind::ConnectionRefused => {
62 // Suggests the service is not ready or available.
63 Self::service_unavailable(Box::new(source))
64 }
65
66 io::ErrorKind::InvalidData | io::ErrorKind::InvalidInput => {
67 // Generally indicates a malformed request.
68 Self::bad_request(Box::new(source))
69 }
70
71 io::ErrorKind::NotFound => {
72 // Indicates a missing resource.
73 Self::not_found(Box::new(source))
74 }
75
76 io::ErrorKind::PermissionDenied => {
77 // Implies restricted access.
78 Self::forbidden(Box::new(source))
79 }
80
81 io::ErrorKind::TimedOut => {
82 // Implies an upstream service timeout.
83 Self::gateway_timeout(Box::new(source))
84 }
85
86 _ => {
87 // Any other kind is treated as an internal server error.
88 Self::internal_server_error(Box::new(source))
89 }
90 }
91 }
92
93 /// Returns a new [`Error`] that will be serialized to JSON when converted to
94 /// a [`Response`].
95 ///
96 pub fn as_json(self) -> Self {
97 Self {
98 as_json: true,
99 ..self
100 }
101 }
102
103 /// Returns a new [`Error`] that will eagerly map the message that will be
104 /// included in the body of the [`Response`] that will be generated from
105 /// self by calling the provided closure. If the closure returns `None`,
106 /// the message will be left unchanged.
107 ///
108 /// # Example
109 ///
110 /// ```
111 /// use via::middleware::error_boundary;
112 /// use via::{Next, Request};
113 ///
114 /// type Error = Box<dyn std::error::Error + Send + Sync>;
115 ///
116 /// #[tokio::main(flavor = "current_thread")]
117 /// async fn main() -> Result<(), Error> {
118 /// let mut app = via::app(());
119 ///
120 /// // Add an `ErrorBoundary` middleware to the route tree that maps
121 /// // errors that occur in subsequent middleware by calling the `redact`
122 /// // function.
123 /// app.include(error_boundary::map(|_, error| {
124 /// error.redact(|message| {
125 /// if message.contains("password") {
126 /// // If password is even mentioned in the error, return an
127 /// // opaque message instead. You'll probably want something
128 /// // more sophisticated than this in production.
129 /// Some("An error occurred...".to_owned())
130 /// } else {
131 /// // Otherwise, use the existing error message.
132 /// None
133 /// }
134 /// })
135 /// }));
136 ///
137 /// Ok(())
138 /// }
139 /// ```
140 ///
141 pub fn redact(self, f: impl FnOnce(&str) -> Option<String>) -> Self {
142 match &self.message {
143 Some(message) => match f(message) {
144 Some(redacted) => self.with_message(redacted),
145 None => self,
146 },
147 None => {
148 let message = self.error.to_string();
149 let redacted = f(&message).unwrap_or(message);
150
151 self.with_message(redacted)
152 }
153 }
154 }
155
156 /// Returns a new [`Error`] that will use the provided message instead of
157 /// calling the [`Display`] implementation of the error source when
158 /// converted to a [`Response`].
159 ///
160 pub fn with_message(self, message: String) -> Self {
161 Self {
162 message: Some(message),
163 ..self
164 }
165 }
166
167 /// Sets the status code of that will be used when converted to a
168 /// [`Response`].
169 ///
170 pub fn with_status(self, status: StatusCode) -> Self {
171 // Placeholder for tracing...
172 // Warn if the status code is not in the 4xx or 5xx range.
173 Self { status, ..self }
174 }
175
176 /// Returns a new [`Error`] that will use the canonical reason phrase of the
177 /// status code as the message included in the [`Response`] body that is
178 /// generated when converted to a [`Response`].
179 ///
180 /// # Example
181 ///
182 /// ```
183 /// use via::middleware::error_boundary;
184 /// use via::{Next, Request};
185 ///
186 /// type Error = Box<dyn std::error::Error + Send + Sync>;
187 ///
188 /// #[tokio::main(flavor = "current_thread")]
189 /// async fn main() -> Result<(), Error> {
190 /// let mut app = via::app(());
191 ///
192 /// // Add an `ErrorBoundary` middleware to the route tree that maps
193 /// // errors that occur in subsequent middleware by calling the
194 /// // `use_canonical_reason` function.
195 /// app.include(error_boundary::map(|_, error| {
196 /// // Prevent error messages that occur in downstream middleware from
197 /// // leaking into the response body by using the reason phrase of
198 /// // the status code associated with the error.
199 /// error.use_canonical_reason()
200 /// }));
201 ///
202 /// Ok(())
203 /// }
204 /// ```
205 ///
206 pub fn use_canonical_reason(self) -> Self {
207 if let Some(reason) = self.status.canonical_reason() {
208 self.with_message(reason.to_owned())
209 } else {
210 // Placeholder for tracing...
211 self.with_message("An error occurred".to_owned())
212 }
213 }
214
215 /// Returns an iterator over the sources of this error.
216 ///
217 pub fn iter(&self) -> impl Iterator<Item = &dyn StdError> {
218 Some(self.source()).into_iter()
219 }
220
221 /// Returns a reference to the error source.
222 ///
223 pub fn source(&self) -> &(dyn StdError + 'static) {
224 &*self.error
225 }
226}
227
228impl Error {
229 /// Returns a new [`Error`] from the provided source that will generate a
230 /// [`Response`] with a `400 Bad Request` status.
231 ///
232 pub fn bad_request(source: DynError) -> Self {
233 Self::new_with_status(StatusCode::BAD_REQUEST, source)
234 }
235
236 /// Returns a new [`Error`] from the provided source that will generate a
237 /// [`Response`] with a `401 Unauthorized` status.
238 ///
239 pub fn unauthorized(source: DynError) -> Self {
240 Self::new_with_status(StatusCode::UNAUTHORIZED, source)
241 }
242
243 /// Returns a new [`Error`] from the provided source that will generate a
244 /// [`Response`] with a `402 Payment Required` status.
245 ///
246 pub fn payment_required(source: DynError) -> Self {
247 Self::new_with_status(StatusCode::PAYMENT_REQUIRED, source)
248 }
249
250 /// Returns a new [`Error`] from the provided source that will generate a
251 /// [`Response`] with a `403 Forbidden` status.
252 ///
253 pub fn forbidden(source: DynError) -> Self {
254 Self::new_with_status(StatusCode::FORBIDDEN, source)
255 }
256
257 /// Returns a new [`Error`] from the provided source that will generate a
258 /// [`Response`] with a `404 Not Found` status.
259 ///
260 pub fn not_found(source: DynError) -> Self {
261 Self::new_with_status(StatusCode::NOT_FOUND, source)
262 }
263
264 /// Returns a new [`Error`] from the provided source that will generate a
265 /// [`Response`] with a `405 Method Not Allowed` status.
266 ///
267 pub fn method_not_allowed(source: DynError) -> Self {
268 Self::new_with_status(StatusCode::METHOD_NOT_ALLOWED, source)
269 }
270
271 /// Returns a new [`Error`] from the provided source that will generate a
272 /// [`Response`] with a `406 Not Acceptable` status.
273 ///
274 pub fn not_acceptable(source: DynError) -> Self {
275 Self::new_with_status(StatusCode::NOT_ACCEPTABLE, source)
276 }
277
278 /// Returns a new [`Error`] from the provided source that will generate a
279 /// [`Response`] with a `407 Proxy Authentication Required` status.
280 ///
281 pub fn proxy_authentication_required(source: DynError) -> Self {
282 Self::new_with_status(StatusCode::PROXY_AUTHENTICATION_REQUIRED, source)
283 }
284
285 /// Returns a new [`Error`] from the provided source that will generate a
286 /// [`Response`] with a `408 Request Timeout` status.
287 ///
288 pub fn request_timeout(source: DynError) -> Self {
289 Self::new_with_status(StatusCode::REQUEST_TIMEOUT, source)
290 }
291
292 /// Returns a new [`Error`] from the provided source that will generate a
293 /// [`Response`] with a `409 Conflict` status.
294 ///
295 pub fn conflict(source: DynError) -> Self {
296 Self::new_with_status(StatusCode::CONFLICT, source)
297 }
298
299 /// Returns a new [`Error`] from the provided source that will generate a
300 /// [`Response`] with a `410 Gone` status.
301 ///
302 pub fn gone(source: DynError) -> Self {
303 Self::new_with_status(StatusCode::GONE, source)
304 }
305
306 /// Returns a new [`Error`] from the provided source that will generate a
307 /// [`Response`] with a `411 Length Required` status.
308 ///
309 pub fn length_required(source: DynError) -> Self {
310 Self::new_with_status(StatusCode::LENGTH_REQUIRED, source)
311 }
312
313 /// Returns a new [`Error`] from the provided source that will generate a
314 /// [`Response`] with a `412 Precondition Failed` status.
315 ///
316 pub fn precondition_failed(source: DynError) -> Self {
317 Self::new_with_status(StatusCode::PRECONDITION_FAILED, source)
318 }
319
320 /// Returns a new [`Error`] from the provided source that will generate a
321 /// [`Response`] with a `413 Payload Too Large` status.
322 ///
323 pub fn payload_too_large(source: DynError) -> Self {
324 Self::new_with_status(StatusCode::PAYLOAD_TOO_LARGE, source)
325 }
326
327 /// Returns a new [`Error`] from the provided source that will generate a
328 /// [`Response`] with a `414 URI Too Long` status.
329 ///
330 pub fn uri_too_long(source: DynError) -> Self {
331 Self::new_with_status(StatusCode::URI_TOO_LONG, source)
332 }
333
334 /// Returns a new [`Error`] from the provided source that will generate a
335 /// [`Response`] with a `415 Unsupported Media Type` status.
336 ///
337 pub fn unsupported_media_type(source: DynError) -> Self {
338 Self::new_with_status(StatusCode::UNSUPPORTED_MEDIA_TYPE, source)
339 }
340
341 /// Returns a new [`Error`] from the provided source that will generate a
342 /// [`Response`] with a `416 Range Not Satisfiable` status.
343 ///
344 pub fn range_not_satisfiable(source: DynError) -> Self {
345 Self::new_with_status(StatusCode::RANGE_NOT_SATISFIABLE, source)
346 }
347
348 /// Returns a new [`Error`] from the provided source that will generate a
349 /// [`Response`] with a `417 Expectation Failed` status.
350 ///
351 pub fn expectation_failed(source: DynError) -> Self {
352 Self::new_with_status(StatusCode::EXPECTATION_FAILED, source)
353 }
354
355 /// Returns a new [`Error`] from the provided source that will generate a
356 /// [`Response`] with a `418 I'm a teapot` status.
357 ///
358 pub fn im_a_teapot(source: DynError) -> Self {
359 Self::new_with_status(StatusCode::IM_A_TEAPOT, source)
360 }
361
362 /// Returns a new [`Error`] from the provided source that will generate a
363 /// [`Response`] with a `421 Misdirected Request` status.
364 ///
365 pub fn misdirected_request(source: DynError) -> Self {
366 Self::new_with_status(StatusCode::MISDIRECTED_REQUEST, source)
367 }
368
369 /// Returns a new [`Error`] from the provided source that will generate a
370 /// [`Response`] with a `422 Unprocessable Entity` status.
371 ///
372 pub fn unprocessable_entity(source: DynError) -> Self {
373 Self::new_with_status(StatusCode::UNPROCESSABLE_ENTITY, source)
374 }
375
376 /// Returns a new [`Error`] from the provided source that will generate a
377 /// [`Response`] with a `423 Locked` status.
378 ///
379 pub fn locked(source: DynError) -> Self {
380 Self::new_with_status(StatusCode::LOCKED, source)
381 }
382
383 /// Returns a new [`Error`] from the provided source that will generate a
384 /// [`Response`] with a `424 Failed Dependency` status.
385 ///
386 pub fn failed_dependency(source: DynError) -> Self {
387 Self::new_with_status(StatusCode::FAILED_DEPENDENCY, source)
388 }
389
390 /// Returns a new [`Error`] from the provided source that will generate a
391 /// [`Response`] with a `426 Upgrade Required` status.
392 ///
393 pub fn upgrade_required(source: DynError) -> Self {
394 Self::new_with_status(StatusCode::UPGRADE_REQUIRED, source)
395 }
396
397 /// Returns a new [`Error`] from the provided source that will generate a
398 /// [`Response`] with a `428 Precondition Required` status.
399 ///
400 pub fn precondition_required(source: DynError) -> Self {
401 Self::new_with_status(StatusCode::PRECONDITION_REQUIRED, source)
402 }
403
404 /// Returns a new [`Error`] from the provided source that will generate a
405 /// [`Response`] with a `429 Too Many Requests` status.
406 ///
407 pub fn too_many_requests(source: DynError) -> Self {
408 Self::new_with_status(StatusCode::TOO_MANY_REQUESTS, source)
409 }
410
411 /// Returns a new [`Error`] from the provided source that will generate a
412 /// [`Response`] with a `431 Request Header Fields Too Large` status.
413 ///
414 pub fn request_header_fields_too_large(source: DynError) -> Self {
415 Self::new_with_status(StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE, source)
416 }
417
418 /// Returns a new [`Error`] from the provided source that will generate a
419 /// [`Response`] with a `451 Unavailable For Legal Reasons` status.
420 ///
421 pub fn unavailable_for_legal_reasons(source: DynError) -> Self {
422 Self::new_with_status(StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS, source)
423 }
424
425 /// Returns a new [`Error`] from the provided source that will generate a
426 /// [`Response`] with a `500 Internal Server Error` status.
427 ///
428 pub fn internal_server_error(source: DynError) -> Self {
429 Self::new_with_status(StatusCode::INTERNAL_SERVER_ERROR, source)
430 }
431
432 /// Returns a new [`Error`] from the provided source that will generate a
433 /// [`Response`] with a `501 Not Implemented` status.
434 ///
435 pub fn not_implemented(source: DynError) -> Self {
436 Self::new_with_status(StatusCode::NOT_IMPLEMENTED, source)
437 }
438
439 /// Returns a new [`Error`] from the provided source that will generate a
440 /// [`Response`] with a `502 Bad Gateway` status.
441 ///
442 pub fn bad_gateway(source: DynError) -> Self {
443 Self::new_with_status(StatusCode::BAD_GATEWAY, source)
444 }
445
446 /// Returns a new [`Error`] from the provided source that will generate a
447 /// [`Response`] with a `503 Service Unavailable` status.
448 ///
449 pub fn service_unavailable(source: DynError) -> Self {
450 Self::new_with_status(StatusCode::SERVICE_UNAVAILABLE, source)
451 }
452
453 /// Returns a new [`Error`] from the provided source that will generate a
454 /// [`Response`] with a `504 Gateway Timeout` status.
455 ///
456 pub fn gateway_timeout(source: DynError) -> Self {
457 Self::new_with_status(StatusCode::GATEWAY_TIMEOUT, source)
458 }
459
460 /// Returns a new [`Error`] from the provided source that will generate a
461 /// [`Response`] with a `505 HTTP Version Not Supported` status.
462 ///
463 pub fn http_version_not_supported(source: DynError) -> Self {
464 Self::new_with_status(StatusCode::HTTP_VERSION_NOT_SUPPORTED, source)
465 }
466
467 /// Returns a new [`Error`] from the provided source that will generate a
468 /// [`Response`] with a `506 Variant Also Negotiates` status.
469 ///
470 pub fn variant_also_negotiates(source: DynError) -> Self {
471 Self::new_with_status(StatusCode::VARIANT_ALSO_NEGOTIATES, source)
472 }
473
474 /// Returns a new [`Error`] from the provided source that will generate a
475 /// [`Response`] with a `507 Insufficient Storage` status.
476 ///
477 pub fn insufficient_storage(source: DynError) -> Self {
478 Self::new_with_status(StatusCode::INSUFFICIENT_STORAGE, source)
479 }
480
481 /// Returns a new [`Error`] from the provided source that will generate a
482 /// [`Response`] with a `508 Loop Detected` status.
483 ///
484 pub fn loop_detected(source: DynError) -> Self {
485 Self::new_with_status(StatusCode::LOOP_DETECTED, source)
486 }
487
488 /// Returns a new [`Error`] from the provided source that will generate a
489 /// [`Response`] with a `510 Not Extended` status.
490 ///
491 pub fn not_extended(source: DynError) -> Self {
492 Self::new_with_status(StatusCode::NOT_EXTENDED, source)
493 }
494
495 /// Returns a new [`Error`] from the provided source that will generate a
496 /// [`Response`] with a `511 Network Authentication Required` status.
497 ///
498 pub fn network_authentication_required(source: DynError) -> Self {
499 Self::new_with_status(StatusCode::NETWORK_AUTHENTICATION_REQUIRED, source)
500 }
501}
502
503impl Error {
504 #[inline]
505 fn new_with_status(status: StatusCode, source: DynError) -> Self {
506 Self {
507 as_json: false,
508 message: None,
509 error: source,
510 status,
511 }
512 }
513}
514
515impl Display for Error {
516 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
517 Display::fmt(&self.error, f)
518 }
519}
520
521impl<T> From<T> for Error
522where
523 T: StdError + Send + Sync + 'static,
524{
525 fn from(source: T) -> Self {
526 Self {
527 as_json: false,
528 message: None,
529 status: StatusCode::INTERNAL_SERVER_ERROR,
530 error: Box::new(source),
531 }
532 }
533}
534
535impl From<Error> for Response {
536 fn from(error: Error) -> Response {
537 let mut respond_with_json = error.as_json;
538
539 loop {
540 if !respond_with_json {
541 let mut response = Response::new(error.to_string().into());
542
543 *response.status_mut() = error.status;
544 break response;
545 }
546
547 match serde_json::to_string(&error)
548 .map_err(Error::from)
549 .and_then(|json| {
550 Response::build()
551 .status(error.status)
552 .header(CONTENT_TYPE, "application/json; charset=utf-8")
553 .body(json)
554 }) {
555 Ok(response) => break response,
556 Err(error) => {
557 respond_with_json = false;
558 // Placeholder for tracing...
559 if cfg!(debug_assertions) {
560 eprintln!("Error: {}", error);
561 }
562 }
563 }
564 }
565 }
566}
567
568impl Serialize for Error {
569 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
570 where
571 S: Serializer,
572 {
573 let mut state = serializer.serialize_struct("Error", 1)?;
574
575 // Serialize the error as a single element array containing an object with
576 // a message field. We do this to provide compatibility with popular API
577 // specification formats like GraphQL and JSON:API.
578 if let Some(message) = &self.message {
579 let errors = [SerializeError { message }];
580 state.serialize_field("errors", &errors)?;
581 } else {
582 let message = self.error.to_string();
583 let errors = [SerializeError { message: &message }];
584
585 state.serialize_field("errors", &errors)?;
586 }
587
588 state.end()
589 }
590}
591
592impl Serialize for SerializeError<'_> {
593 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
594 where
595 S: Serializer,
596 {
597 let mut state = serializer.serialize_struct("ErrorMessage", 1)?;
598
599 state.serialize_field("message", &self.message)?;
600 state.end()
601 }
602}