http_kit/error.rs
1//! Error types and utilities.
2//!
3//! This module provides the core error handling infrastructure. The main types are:
4//!
5//! - [`Error`] - The main error type used throughout HTTP operations
6//! - [`Result`] - A specialized Result type alias for HTTP operations
7//! - [`ResultExt`] - Extension trait that adds HTTP status code handling
8//!
9//! The error types integrate with standard Rust error handling while adding HTTP-specific
10//! functionality like status codes.
11//!
12//! # Examples
13//!
14//! ```rust
15//! use http_kit::{Error, Result, ResultExt};
16//! use http::StatusCode;
17//!
18//! // Create an error with a status code
19//! let err = Error::msg("not found").set_status(StatusCode::NOT_FOUND);
20//!
21//! // Add status code to existing Result
22//! let result: Result<()> = Ok::<(), std::convert::Infallible>(()).status(StatusCode::OK);
23//! ```
24//!
25use alloc::boxed::Box;
26use core::{
27 fmt,
28 ops::{Deref, DerefMut},
29};
30use http::StatusCode;
31
32/// The main error type for HTTP operations.
33///
34/// This error type wraps any error with an associated HTTP status code,
35/// providing both the underlying error information and the appropriate
36/// HTTP response status.
37///
38/// # Examples
39///
40/// ```rust
41/// use http_kit::Error;
42/// use http::StatusCode;
43///
44/// // Create from a string message
45/// let err = Error::msg("Something went wrong");
46///
47/// // Create with a specific status code
48/// let err = Error::msg("Not found").set_status(StatusCode::NOT_FOUND);
49/// ```
50pub struct Error {
51 error: anyhow::Error,
52 status: StatusCode,
53}
54
55/// A specialized Result type for HTTP operations.
56///
57/// This is a convenience alias for `Result<T, Error>` that's used throughout
58/// the HTTP toolkit to simplify error handling in HTTP contexts.
59///
60/// # Examples
61///
62/// ```rust
63/// use http_kit::{Result, Error};
64/// use http::StatusCode;
65///
66/// fn example_function() -> Result<String> {
67/// Ok("success".to_string())
68/// }
69///
70/// fn failing_function() -> Result<()> {
71/// Err(Error::msg("failed").set_status(StatusCode::INTERNAL_SERVER_ERROR))
72/// }
73/// ```
74pub type Result<T> = core::result::Result<T, Error>;
75
76impl Error {
77 /// Creates a new `Error` from any error type with the given HTTP status code.
78 ///
79 /// # Arguments
80 ///
81 /// * `error` - Any error type that can be converted to `anyhow::Error`
82 /// * `status` - HTTP status code (or value convertible to one)
83 ///
84 /// # Panics
85 ///
86 /// Panics if the status code is invalid.
87 ///
88 /// # Examples
89 ///
90 /// ```rust
91 /// use http_kit::Error;
92 /// use http::StatusCode;
93 /// use std::io;
94 ///
95 /// let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
96 /// let http_err = Error::new(io_err, StatusCode::NOT_FOUND);
97 /// ```
98 pub fn new<E, S>(error: E, status: S) -> Self
99 where
100 E: Into<anyhow::Error>,
101 S: TryInto<StatusCode>,
102 S::Error: fmt::Debug,
103 {
104 Self {
105 error: error.into(),
106 status: status.try_into().unwrap(), //may panic if user delivers an illegal code.
107 }
108 }
109
110 /// Creates an `Error` from a message string with a default status code.
111 ///
112 /// The default status code is `SERVICE_UNAVAILABLE` (503).
113 ///
114 /// # Arguments
115 ///
116 /// * `msg` - Any type that implements `Display + Debug + Send + Sync + 'static`
117 ///
118 /// # Examples
119 ///
120 /// ```rust
121 /// use http_kit::Error;
122 ///
123 /// let err = Error::msg("Something went wrong");
124 /// let err = Error::msg(format!("Failed to process item {}", 42));
125 /// ```
126 pub fn msg<S>(msg: S) -> Self
127 where
128 S: fmt::Display + fmt::Debug + Send + Sync + 'static,
129 {
130 anyhow::Error::msg(msg).into()
131 }
132
133 /// Sets the HTTP status code of this error.
134 ///
135 /// Only error status codes (400-599) can be set. In debug builds,
136 /// this method will assert that the status code is in the valid range.
137 ///
138 /// # Arguments
139 ///
140 /// * `status` - HTTP status code (or value convertible to one)
141 ///
142 /// # Panics
143 ///
144 /// Panics if the status code is invalid or not an error status code.
145 ///
146 /// # Examples
147 ///
148 /// ```rust
149 /// use http_kit::Error;
150 /// use http::StatusCode;
151 ///
152 /// let err = Error::msg("Not found").set_status(StatusCode::NOT_FOUND);
153 /// ```
154 pub fn set_status<S>(mut self, status: S) -> Self
155 where
156 S: TryInto<StatusCode>,
157 S::Error: fmt::Debug,
158 {
159 let status = status.try_into().expect("Invalid status code");
160 if cfg!(debug_assertions) {
161 assert!(
162 (400..=599).contains(&status.as_u16()),
163 "Expected a status code within 400~599"
164 )
165 }
166
167 self.status = status;
168
169 self
170 }
171
172 /// Returns the HTTP status code associated with this error.
173 ///
174 /// # Examples
175 ///
176 /// ```rust
177 /// use http_kit::Error;
178 /// use http::StatusCode;
179 ///
180 /// let err = Error::msg("not found").set_status(StatusCode::NOT_FOUND);
181 /// assert_eq!(err.status(), StatusCode::NOT_FOUND);
182 /// ```
183 pub fn status(&self) -> StatusCode {
184 self.status
185 }
186
187 /// Attempts to downcast the inner error to a concrete type.
188 ///
189 /// Returns `Ok(Box<E>)` if the downcast succeeds, or `Err(Self)` if it fails.
190 ///
191 /// # Examples
192 ///
193 /// ```rust
194 /// use http_kit::Error;
195 /// use http::StatusCode;
196 /// use std::io;
197 ///
198 /// let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
199 /// let http_err = Error::new(io_err, StatusCode::NOT_FOUND);
200 ///
201 /// match http_err.downcast::<io::Error>() {
202 /// Ok(io_error) => println!("Got IO error: {}", io_error),
203 /// Err(original) => println!("Not an IO error: {}", original),
204 /// }
205 /// ```
206 pub fn downcast<E>(self) -> core::result::Result<Box<E>, Self>
207 where
208 E: core::error::Error + Send + Sync + 'static,
209 {
210 let Self { status, error } = self;
211 error.downcast().map_err(|error| Self { status, error })
212 }
213
214 /// Attempts to downcast the inner error to a reference of the concrete type.
215 ///
216 /// Returns `Some(&E)` if the downcast succeeds, or `None` if it fails.
217 ///
218 /// # Examples
219 ///
220 /// ```rust
221 /// use http_kit::Error;
222 /// use http::StatusCode;
223 /// use std::io;
224 ///
225 /// let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
226 /// let http_err = Error::new(io_err, StatusCode::NOT_FOUND);
227 ///
228 /// if let Some(io_error) = http_err.downcast_ref::<io::Error>() {
229 /// println!("IO error kind: {:?}", io_error.kind());
230 /// }
231 /// ```
232 pub fn downcast_ref<E>(&self) -> Option<&E>
233 where
234 E: core::error::Error + Send + Sync + 'static,
235 {
236 self.error.downcast_ref()
237 }
238
239 /// Attempts to downcast the inner error to a mutable reference of the concrete type.
240 ///
241 /// Returns `Some(&mut E)` if the downcast succeeds, or `None` if it fails.
242 ///
243 /// # Examples
244 ///
245 /// ```rust
246 /// use http_kit::Error;
247 /// use http::StatusCode;
248 /// use std::io;
249 ///
250 /// let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
251 /// let mut http_err = Error::new(io_err, StatusCode::NOT_FOUND);
252 ///
253 /// if let Some(io_error) = http_err.downcast_mut::<io::Error>() {
254 /// // Modify the IO error if needed
255 /// }
256 /// ```
257 pub fn downcast_mut<E>(&mut self) -> Option<&mut E>
258 where
259 E: core::error::Error + Send + Sync + 'static,
260 {
261 self.error.downcast_mut()
262 }
263
264 /// Consumes this error and returns the inner error, discarding the status code.
265 ///
266 /// # Examples
267 ///
268 /// ```rust
269 /// use http_kit::Error;
270 /// use http::StatusCode;
271 ///
272 /// let err = Error::msg("some error").set_status(StatusCode::BAD_REQUEST);
273 /// let inner = err.into_inner();
274 /// ```
275 pub fn into_inner(self) -> Box<dyn core::error::Error + Send + Sync + 'static> {
276 self.error.into()
277 }
278}
279
280impl<E: Into<anyhow::Error>> From<E> for Error {
281 fn from(error: E) -> Self {
282 Self::new(error, StatusCode::SERVICE_UNAVAILABLE)
283 }
284}
285
286impl From<Error> for Box<dyn core::error::Error> {
287 fn from(error: Error) -> Self {
288 error.error.into()
289 }
290}
291
292impl fmt::Debug for Error {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 fmt::Debug::fmt(&self.error, f)
295 }
296}
297
298impl fmt::Display for Error {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 fmt::Display::fmt(&self.error, f)
301 }
302}
303
304impl AsRef<dyn core::error::Error + Send + 'static> for Error {
305 fn as_ref(&self) -> &(dyn core::error::Error + Send + 'static) {
306 self.deref()
307 }
308}
309
310impl AsMut<dyn core::error::Error + Send + 'static> for Error {
311 fn as_mut(&mut self) -> &mut (dyn core::error::Error + Send + 'static) {
312 self.deref_mut()
313 }
314}
315
316impl Deref for Error {
317 type Target = dyn core::error::Error + Send + 'static;
318
319 fn deref(&self) -> &Self::Target {
320 self.error.deref()
321 }
322}
323
324impl DerefMut for Error {
325 fn deref_mut(&mut self) -> &mut Self::Target {
326 self.error.deref_mut()
327 }
328}
329
330/// Extension trait that adds HTTP status code handling to `Result` and `Option` types.
331///
332/// This trait provides a convenient `status` method that allows you to associate
333/// an HTTP status code with errors when converting them to the HTTP toolkit's
334/// `Result` type.
335///
336/// # Examples
337///
338/// ```rust
339/// use http_kit::{ResultExt, Result};
340/// use http::StatusCode;
341/// use std::fs;
342///
343/// fn read_config() -> Result<String> {
344/// fs::read_to_string("config.txt")
345/// .status(StatusCode::NOT_FOUND)
346/// }
347///
348/// fn get_user_id() -> Result<u32> {
349/// Some(42_u32)
350/// .status(StatusCode::BAD_REQUEST)
351/// }
352/// ```
353pub trait ResultExt<T>
354where
355 Self: Sized,
356{
357 /// Associates an HTTP status code with an error or None value.
358 ///
359 /// For `Result` types, this wraps any error with the specified status code.
360 /// For `Option` types, this converts `None` to an error with the specified status code.
361 ///
362 /// # Arguments
363 ///
364 /// * `status` - HTTP status code to associate with the error
365 ///
366 /// # Examples
367 ///
368 /// ```rust
369 /// use http_kit::{ResultExt, Result};
370 /// use http::StatusCode;
371 /// use std::fs;
372 ///
373 /// // With Result
374 /// let result: Result<String> = fs::read_to_string("missing.txt")
375 /// .status(StatusCode::NOT_FOUND);
376 ///
377 /// // With Option
378 /// let result: Result<i32> = None
379 /// .status(StatusCode::BAD_REQUEST);
380 /// ```
381 fn status<S>(self, status: S) -> Result<T>
382 where
383 S: TryInto<StatusCode>,
384 S::Error: fmt::Debug;
385}
386
387impl<T, E> ResultExt<T> for core::result::Result<T, E>
388where
389 E: core::error::Error + Send + Sync + 'static,
390{
391 fn status<S>(self, status: S) -> Result<T>
392 where
393 S: TryInto<StatusCode>,
394 S::Error: fmt::Debug,
395 {
396 self.map_err(|error| Error::new(error, status))
397 }
398}
399
400impl<T> ResultExt<T> for Option<T> {
401 fn status<S>(self, status: S) -> Result<T>
402 where
403 S: TryInto<StatusCode>,
404 S::Error: fmt::Debug,
405 {
406 self.ok_or(Error::msg("None Error").set_status(status))
407 }
408}
409
410/// Constructs an error with a formatted message and an associated HTTP status code.
411///
412/// This macro simplifies the creation of error values that include both a custom message
413/// (formatted using the standard Rust formatting syntax) and an HTTP status code. The status
414/// code is converted into a [`StatusCode`] type, and the message is wrapped in an [`Error`].
415/// The resulting error has its status set accordingly.
416///
417/// # Arguments
418///
419/// * `$fmt` - A format string, as used in [`format!`], describing how to format the error message.
420/// * `$status` - An expression that can be converted into a [`StatusCode`]. If the conversion fails,
421/// the macro will panic with "Invalid status code".
422/// * `$args` - Zero or more additional arguments for the format string.
423///
424/// # Example
425///
426/// ```rust
427/// msg!("Resource not found: {}", 404, resource_id);
428/// ```
429///
430/// This will create an error with the message "Resource not found: {resource_id}" and set the status code to 404.
431///
432/// # Panics
433///
434/// Panics if `$status` cannot be converted into a valid [`StatusCode`].
435///
436/// # Notes
437///
438/// - The macro requires that `$crate::StatusCode` and `$crate::Error` are available in scope.
439/// - The macro uses `alloc::format!` for message formatting, so it requires the `alloc` crate.
440///
441/// [`StatusCode`]: crate::StatusCode
442/// [`Error`]: crate::Error
443/// [`format!`]: https://doc.rust-lang.org/std/macro.format.html
444#[macro_export]
445macro_rules! msg {
446 ($fmt:expr,$status:expr $(, $args:expr)* $(,)?) => {
447 let status: $crate::StatusCode = $status.try_into().expect("Invalid status code");
448 let message = alloc::format!($fmt $(, $args)*);
449 let error = $crate::Error::msg(message);
450 error.set_status(status)
451 };
452}