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}