hyperdrive/error.rs
1use crate::{BoxedError, DefaultFuture};
2use futures::IntoFuture;
3use http::StatusCode;
4use std::{borrow::Cow, error, fmt};
5
6/// The error type used by the Hyperdrive library.
7///
8/// This type can be turned into an HTTP response and sent back to a client.
9#[derive(Debug)]
10pub struct Error {
11 status: StatusCode,
12 /// In case of a `405 Method Not Allowed` error, stores the allowed HTTP
13 /// methods.
14 allowed_methods: Cow<'static, [&'static http::Method]>,
15 source: Option<BoxedError>,
16}
17
18impl Error {
19 fn new(
20 status: StatusCode,
21 allowed_methods: Cow<'static, [&'static http::Method]>,
22 source: Option<BoxedError>,
23 ) -> Self {
24 assert!(
25 status.is_client_error() || status.is_server_error(),
26 "hyperdrive::Error must be created with an error status, not {}",
27 status,
28 );
29
30 Self {
31 status,
32 allowed_methods,
33 source,
34 }
35 }
36
37 /// Creates an error that contains just the given `StatusCode`.
38 ///
39 /// # Panics
40 ///
41 /// This will panic when called with a `status` that does not indicate a
42 /// client or server error.
43 pub fn from_status(status: StatusCode) -> Self {
44 Self::new(status, (&[][..]).into(), None)
45 }
46
47 /// Creates an error from an HTTP error code and an underlying error that
48 /// caused this one.
49 ///
50 /// Responding with the returned `Error` will not send a response body back
51 /// to the client.
52 ///
53 /// # Parameters
54 ///
55 /// * **`status`**: The HTTP `StatusCode` describing the error.
56 /// * **`source`**: The underlying error that caused this one. Any type
57 /// implementing `std::error::Error + Send + Sync` can be passed here.
58 ///
59 /// # Panics
60 ///
61 /// This will panic when called with a `status` that does not indicate a
62 /// client or server error.
63 pub fn with_source<S>(status: StatusCode, source: S) -> Self
64 where
65 S: Into<BoxedError>,
66 {
67 Self::new(status, (&[][..]).into(), Some(source.into()))
68 }
69
70 /// Creates an error with status code `405 Method Not Allowed` and includes
71 /// the allowed set of HTTP methods.
72 ///
73 /// This is called by the code generated by `#[derive(FromRequest)]` and
74 /// usually does not need to be called by the user (it may be difficult to
75 /// determine the full set of allowed methods for a given path).
76 ///
77 /// Calling `Error::response` on the error created by this function will
78 /// automatically include an `Allow` header listing all allowed methods.
79 /// Including this header is [required] by RFC 7231.
80 ///
81 /// # Parameters
82 ///
83 /// * **`allowed_methods`**: The list of allowed HTTP methods for the path
84 /// in the request. This can be empty, but usually should contain at least
85 /// one method.
86 ///
87 /// [required]: https://tools.ietf.org/html/rfc7231#section-6.5.5
88 pub fn wrong_method<M>(allowed_methods: M) -> Self
89 where
90 M: Into<Cow<'static, [&'static http::Method]>>,
91 {
92 Self::new(StatusCode::METHOD_NOT_ALLOWED, allowed_methods.into(), None)
93 }
94
95 /// Returns the HTTP status code that describes this error.
96 pub fn http_status(&self) -> StatusCode {
97 self.status
98 }
99
100 /// Creates an HTTP response for indicating this error to the client.
101 ///
102 /// No body will be provided (hence the `()` body type), but the caller can
103 /// `map` the result to supply one.
104 ///
105 /// # Example
106 ///
107 /// Call `map` on the response to supply your own HTTP payload:
108 ///
109 /// ```
110 /// # use hyperdrive::Error;
111 /// use http::StatusCode;
112 /// use hyper::Body;
113 ///
114 /// let error = Error::from_status(StatusCode::NOT_FOUND);
115 /// let response = error.response()
116 /// .map(|()| Body::from("oh no!"));
117 /// ```
118 pub fn response(&self) -> http::Response<()> {
119 let mut builder = http::Response::builder();
120 builder.status(self.http_status());
121
122 if self.status == StatusCode::METHOD_NOT_ALLOWED {
123 // The spec mandates that "405 Method Not Allowed" always sends an
124 // `Allow` header (it may be empty, though).
125 let allowed = self
126 .allowed_methods
127 .iter()
128 .map(|method| method.as_str().to_uppercase())
129 .collect::<Vec<_>>()
130 .join(", ");
131 builder.header(http::header::ALLOW, allowed);
132 }
133
134 builder
135 .body(())
136 .expect("could not build HTTP response for error")
137 }
138
139 /// Turns this error into a generic boxed future compatible with the output
140 /// of `#[derive(FromRequest)]`.
141 ///
142 /// This is used by the code generated by `#[derive(FromRequest)]`.
143 #[doc(hidden)] // not part of public API
144 pub fn into_future<T: Send + 'static>(self) -> DefaultFuture<T, BoxedError> {
145 Box::new(Err(BoxedError::from(self)).into_future())
146 }
147
148 /// If `self` is a `405 Method Not Allowed` error, returns the list of
149 /// allowed methods.
150 ///
151 /// Returns `None` if `self` is a different kind of error.
152 pub fn allowed_methods(&self) -> Option<&[&'static http::Method]> {
153 if self.status == StatusCode::METHOD_NOT_ALLOWED {
154 Some(&self.allowed_methods)
155 } else {
156 None
157 }
158 }
159}
160
161impl fmt::Display for Error {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 match &self.source {
164 None => write!(f, "{}", self.status),
165 Some(source) => write!(f, "{}: {}", self.status, source),
166 }
167 }
168}
169
170impl error::Error for Error {
171 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
172 match &self.source {
173 Some(source) => Some(&**source),
174 None => None,
175 }
176 }
177}