Skip to main content

churust_core/
error.rs

1//! The status-carrying [`Error`] type and the crate-wide [`Result`] alias.
2//!
3//! Churust errors render directly into HTTP responses: each [`Error`] carries
4//! the [`StatusCode`] to send, a human-readable message used as the response
5//! body, an optional source error for diagnostics, and any headers to attach
6//! (for example `WWW-Authenticate` on a `401`). Handlers and extractors return
7//! `Result<T>`; when an `Err` reaches the pipeline it is converted via
8//! [`IntoResponse`](crate::IntoResponse).
9
10use http::header::{HeaderName, HeaderValue};
11use http::StatusCode;
12use std::fmt;
13
14/// Crate-wide result type, defaulting the error to [`Error`].
15///
16/// Handlers and extractors typically return `Result<T>` (i.e.
17/// `Result<T, Error>`); the second type parameter exists only so the alias can
18/// be reused with a different error type when desired.
19///
20/// ```
21/// use churust_core::{Error, Result};
22/// use http::StatusCode;
23///
24/// fn parse_age(raw: &str) -> Result<u8> {
25///     raw.parse()
26///         .map_err(|_| Error::bad_request("age must be a number"))
27/// }
28///
29/// assert!(parse_age("30").is_ok());
30/// assert_eq!(parse_age("nope").unwrap_err().status(), StatusCode::BAD_REQUEST);
31/// ```
32pub type Result<T, E = Error> = std::result::Result<T, E>;
33
34/// A handler/framework error carrying the HTTP status to respond with.
35///
36/// An `Error` bundles everything needed to render a failed request: the
37/// [`StatusCode`], a message (sent as the response body), an optional source
38/// error, and headers to attach to the rendered response. Return one from any
39/// handler or extractor and the pipeline turns it into a [`Response`](crate::Response) for you
40/// via the [`IntoResponse`](crate::IntoResponse) impl.
41///
42/// Use the constructors ([`Error::bad_request`], [`Error::not_found`],
43/// [`Error::internal`], or [`Error::new`] for an arbitrary status) and chain
44/// [`with_source`](Error::with_source) /
45/// [`with_response_header`](Error::with_response_header) as needed.
46///
47/// ```
48/// use churust_core::Error;
49/// use http::StatusCode;
50///
51/// let err = Error::not_found("user 42 does not exist");
52/// assert_eq!(err.status(), StatusCode::NOT_FOUND);
53/// assert_eq!(err.message(), "user 42 does not exist");
54/// ```
55#[derive(Debug)]
56pub struct Error {
57    status: StatusCode,
58    message: String,
59    source: Option<Box<dyn std::error::Error + Send + Sync>>,
60    headers: Vec<(HeaderName, HeaderValue)>,
61}
62
63impl Error {
64    /// Create an error with an explicit `status` and `message`.
65    ///
66    /// Prefer the named constructors ([`bad_request`](Error::bad_request),
67    /// [`not_found`](Error::not_found), [`internal`](Error::internal)) for the
68    /// common cases; use `new` when you need any other status (e.g. `401` or
69    /// `409`).
70    ///
71    /// ```
72    /// use churust_core::Error;
73    /// use http::StatusCode;
74    ///
75    /// let err = Error::new(StatusCode::CONFLICT, "already exists");
76    /// assert_eq!(err.status(), StatusCode::CONFLICT);
77    /// ```
78    pub fn new(status: StatusCode, message: impl Into<String>) -> Self {
79        Self {
80            status,
81            message: message.into(),
82            source: None,
83            headers: Vec::new(),
84        }
85    }
86    /// Create a `400 Bad Request` error — use for malformed input such as an
87    /// unparseable path parameter or invalid query string.
88    ///
89    /// ```
90    /// use churust_core::Error;
91    /// use http::StatusCode;
92    ///
93    /// assert_eq!(Error::bad_request("nope").status(), StatusCode::BAD_REQUEST);
94    /// ```
95    pub fn bad_request(message: impl Into<String>) -> Self {
96        Self::new(StatusCode::BAD_REQUEST, message)
97    }
98    /// Create a `404 Not Found` error — use when a requested resource does not
99    /// exist.
100    ///
101    /// ```
102    /// use churust_core::Error;
103    /// use http::StatusCode;
104    ///
105    /// assert_eq!(Error::not_found("gone").status(), StatusCode::NOT_FOUND);
106    /// ```
107    pub fn not_found(message: impl Into<String>) -> Self {
108        Self::new(StatusCode::NOT_FOUND, message)
109    }
110    /// Create a `500 Internal Server Error` — use for unexpected server-side
111    /// failures (e.g. a missing application-state dependency).
112    ///
113    /// ```
114    /// use churust_core::Error;
115    /// use http::StatusCode;
116    ///
117    /// assert_eq!(Error::internal("boom").status(), StatusCode::INTERNAL_SERVER_ERROR);
118    /// ```
119    pub fn internal(message: impl Into<String>) -> Self {
120        Self::new(StatusCode::INTERNAL_SERVER_ERROR, message)
121    }
122    /// Attach an underlying source error for diagnostics (exposed via
123    /// [`std::error::Error::source`]). The source does not change the rendered
124    /// response; it is for logging and error chaining. Returns `self` so it can
125    /// be chained.
126    ///
127    /// ```
128    /// use churust_core::Error;
129    /// use std::error::Error as _;
130    ///
131    /// let io = std::io::Error::new(std::io::ErrorKind::Other, "disk gone");
132    /// let err = Error::internal("write failed").with_source(io);
133    /// assert!(err.source().is_some());
134    /// ```
135    pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
136        self.source = Some(Box::new(source));
137        self
138    }
139    /// The HTTP status this error renders to.
140    ///
141    /// ```
142    /// use churust_core::Error;
143    /// use http::StatusCode;
144    ///
145    /// assert_eq!(Error::bad_request("x").status(), StatusCode::BAD_REQUEST);
146    /// ```
147    pub fn status(&self) -> StatusCode {
148        self.status
149    }
150    /// The human-readable message, used as the response body when rendered.
151    ///
152    /// ```
153    /// use churust_core::Error;
154    ///
155    /// assert_eq!(Error::not_found("missing").message(), "missing");
156    /// ```
157    pub fn message(&self) -> &str {
158        &self.message
159    }
160
161    /// Attach a header to the response this error renders into (e.g.
162    /// `WWW-Authenticate` on a `401`). May be called repeatedly to add several
163    /// headers; returns `self` for chaining.
164    ///
165    /// ```
166    /// use churust_core::Error;
167    /// use http::{StatusCode, header::WWW_AUTHENTICATE, HeaderValue};
168    ///
169    /// let err = Error::new(StatusCode::UNAUTHORIZED, "login required")
170    ///     .with_response_header(WWW_AUTHENTICATE, HeaderValue::from_static("Bearer"));
171    /// assert_eq!(err.response_headers().len(), 1);
172    /// ```
173    pub fn with_response_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
174        self.headers.push((name, value));
175        self
176    }
177
178    /// The headers to apply when rendering this error to a
179    /// [`Response`](crate::Response).
180    /// Returns an empty slice unless
181    /// [`with_response_header`](Error::with_response_header) was called.
182    ///
183    /// ```
184    /// use churust_core::Error;
185    ///
186    /// assert!(Error::bad_request("x").response_headers().is_empty());
187    /// ```
188    pub fn response_headers(&self) -> &[(HeaderName, HeaderValue)] {
189        &self.headers
190    }
191}
192
193impl fmt::Display for Error {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        write!(f, "{}: {}", self.status, self.message)
196    }
197}
198
199impl std::error::Error for Error {
200    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
201        self.source
202            .as_ref()
203            .map(|s| s.as_ref() as &(dyn std::error::Error + 'static))
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn carries_status_and_message() {
213        let e = Error::not_found("nope");
214        assert_eq!(e.status(), StatusCode::NOT_FOUND);
215        assert_eq!(e.message(), "nope");
216    }
217
218    #[test]
219    fn display_includes_status() {
220        let e = Error::bad_request("bad");
221        assert!(format!("{e}").contains("400"));
222    }
223
224    #[test]
225    fn carries_response_headers() {
226        let e = Error::new(StatusCode::UNAUTHORIZED, "no").with_response_header(
227            http::header::WWW_AUTHENTICATE,
228            http::HeaderValue::from_static("Bearer"),
229        );
230        assert_eq!(e.response_headers().len(), 1);
231    }
232}