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}