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}