rama_error/lib.rs
1//! Error utilities for rama and its users.
2//!
3//! Crate used by the end-user `rama` crate and `rama` crate authors alike.
4//!
5//! Learn more about `rama`:
6//!
7//! - Github: <https://github.com/plabayo/rama>
8//! - Book: <https://ramaproxy.org/book/>
9//!
10//! # Errors
11//!
12//! Errors in Rust are a bit ambiguous:
13//!
14//! - the infamous `Result<T, E>` is a type that can either be `Ok(T)` or `Err(E)`, where `E` is
15//! the error type in case something went wrong.
16//! - the [`std::error::Error`] trait is a trait that represents errors that can be displayed and
17//! have a source (cause).
18//!
19//! The ambiguity comes from the fact that the [`std::error::Error`] trait is not required to be
20//! implemented for the error type `E` in the `Result<T, E>` type. This means that one can have
21//! a `Result<T, E>` where `E` is not an error type. A common example of something else it can be
22//! is that it has the same type as the `T` type, which is not an error type. E.g. in case of a web
23//! service middleware a firewall could return a 403 Http response as the `Err` variant of the
24//! `Result<T, Response>`. Where `T` is most likely also a `Response` type. In which
25//! case you might as well have `Result<Response, Infallible>`.
26//!
27//! Within Web Services we usually do not want an error type, as it does not make any sense.
28//! This is because the server has to respond something (unless you simply want to kill the connection),
29//! and so it makes much more sense to enforce the code type-wise to always return a response.
30//!
31//! The most tricky scenario, if you can call it that, is what to do for middleware services.
32//! These situations are tricky because they can wrap any generic `S` type, where `S` is the
33//! service type. This means that the error type can be anything, and so it is not possible to
34//! create values of that type for scenarios where the error comes from the middleware itself.
35//!
36//! There are several possibilities here and we'll go over them next. But before we do that,
37//! I do want to emphasise that while Rust's `Result<T, E>` does not enforce that `E` is an error
38//! type, it is still a good practice to use an error type for the `E` type. And that is also
39//! that as a rule of thumb we do in Rama.
40//!
41//! ## Type Erasure
42//!
43//! The [`BoxError`] type alias is a boxed Error trait object and can be used to represent any error that
44//! implements the [`std::error::Error`] trait and is used for cases where it is usually not
45//! that important what specific error type is returned, but rather that an error occurred.
46//! Boxed errors do allow to _downcast_ to check for concrete error types, but this checks
47//! only the top-level error and not the cause chain.
48//!
49//! ## Error Extension
50//!
51//! The [`ErrorExt`] trait provides a set of methods to work with errors. These methods are
52//! implemented for all types that implement the [`std::error::Error`] trait. The methods are
53//! used to add context to an error, add a backtrace to an error, and to convert an error into
54//! an opaque error.
55//!
56//! ## Opaque Error
57//!
58//! The [`OpaqueError`] type is a type-erased error that can be used to represent any error
59//! that implements the [`std::error::Error`] trait. Using the [`OpaqueError::from_display`]
60//! you can even create errors from a displayable type.
61//!
62//! The other advantage of [`OpaqueError`] over [`BoxError`]
63//! is that it is Sized and can be used in places where a `Sized`` type is required,
64//! while [`BoxError`] is `?Sized` and can give you a hard time in certain scenarios.
65//!
66//! ## `error` macro
67//!
68//! The `error` macro is a convenient way to create an [`OpaqueError`]
69//! from an error, format string or displayable type.
70//!
71//! ### `error` macro Example
72//!
73//! ```rust
74//! use rama_error::{error, ErrorExt, OpaqueError};
75//!
76//! let error = error!("error").context("foo");
77//! assert_eq!(error.to_string(), "foo\r\n ↪ error");
78//!
79//! let error = error!("error {}", 404).context("foo");
80//! assert_eq!(error.to_string(), "foo\r\n ↪ error 404");
81//!
82//! #[derive(Debug)]
83//! struct CustomError;
84//!
85//! impl std::fmt::Display for CustomError {
86//! fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
87//! write!(f, "entity not found")
88//! }
89//! }
90//!
91//! impl std::error::Error for CustomError {}
92//!
93//! let error = error!(CustomError).context("foo");
94//!
95//! assert_eq!(error.to_string(), "foo\r\n ↪ entity not found");
96//! ```
97//!
98//! ## Error Context
99//!
100//! The [`ErrorContext`] allows you to add a context to [`Result`]
101//! and [`Option`] types:
102//!
103//! - For [`Result`] types, the context is added to the error variant,
104//! turning `Result<T, E>` into `Result<T, OpaqueError>`;
105//! - For [`Option`] types, the context is used as a DisplayError when
106//! the open is `None`, turning `Option<T>` into `Result<T, OpaqueError>`.
107//!
108//! This is useful when you want to add custom context.
109//! And can also be combined with other [`ErrorExt`] methods,
110//! such as [`ErrorExt::backtrace`] to add even more info to the error case,
111//! if there is one.
112//!
113//! It is also an easy way to turn an option value into the inner value,
114//! short-circuiting using `?` with the new context (Display) error
115//! when the option was `None`.
116//!
117//! ### Error Context Example
118//!
119//! Option Example:
120//!
121//! ```rust
122//! use rama_error::{ErrorContext, ErrorExt};
123//!
124//! let value = Some(42);
125//! let value = match value.context("value is None") {
126//! Ok(value) => assert_eq!(value, 42),
127//! Err(error) => panic!("unexpected error: {error}"),
128//! };
129//!
130//! let value: Option<usize> = None;
131//! let result = value.context("value is None");
132//! assert!(result.is_err());
133//! ```
134//!
135//! Result Example:
136//!
137//! ```rust
138//! use rama_error::{ErrorContext, ErrorExt, OpaqueError};
139//!
140//! let value: Result<_, OpaqueError> = Ok(42);
141//! let value = match value.context("get the answer") {
142//! Ok(value) => assert_eq!(value, 42),
143//! Err(error) => panic!("unexpected error: {error}"),
144//! };
145//!
146//! let value: Result<usize, _> = Err(OpaqueError::from_display("error"));
147//! let result = value.context("get the answer");
148//! assert!(result.is_err());
149//! ```
150//!
151//! ## Error Composition
152//!
153//! Sometimes it can be useful to compose errors with more
154//! expressive error types. In such cases [`OpaqueError`] is... too opaque.
155//!
156//! In an early design of Rama we considered adding a `compose_error` function macro
157//! that would allow to create error types in a similar manner as [the `thiserror` crate](https://docs.rs/thiserror),
158//! but we decided against it as it would be an abstraction too much.
159//!
160//! Rama was created to give developers the full power of the Rust language to develop
161//! proxies, and by extension also web services and http clients. In a similar line of thought
162//! it is also important that one has all tools available to create the error types for their purpose.
163//!
164//! As such, if you want your own custom error types we recommend just creating them
165//! as you would any other type in Rust. The blog article <https://sabrinajewson.org/blog/errors>
166//! gives a good overview and background on this topic.
167//!
168//! You can declare your own `macro_rules` in case there are common patterns for the services
169//! and middlewares that you are writing for your project. For inspiration you can
170//! see the http rejection macros we borrowed and modified from
171//! [Axum's extract logic](https://github.com/tokio-rs/axum/blob/5201798d4e4d4759c208ef83e30ce85820c07baa/axum-core/src/macros.rs):
172//! <https://github.com/plabayo/rama/blob/main/rama-http/src/utils/macros/http_error.rs>
173//!
174//! And of course... if you really want, against our advice in,
175//! you can use [the `thiserror` crate](https://docs.rs/thiserror),
176//! or even [the `anyhow` crate](https://docs.rs/anyhow). All is possible.
177
178#![doc(
179 html_favicon_url = "https://raw.githubusercontent.com/plabayo/rama/main/docs/img/old_logo.png"
180)]
181#![doc(html_logo_url = "https://raw.githubusercontent.com/plabayo/rama/main/docs/img/old_logo.png")]
182#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
183#![cfg_attr(test, allow(clippy::float_cmp))]
184#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))]
185
186use std::error::Error as StdError;
187
188/// Alias for a type-erased error type.
189///
190/// See the [module level documentation](crate::error) for more information.
191pub type BoxError = Box<dyn StdError + Send + Sync>;
192
193mod ext;
194pub use ext::{ErrorContext, ErrorExt, OpaqueError};
195
196mod macros;
197#[doc(inline)]
198pub use macros::error;
199
200#[cfg(test)]
201mod test {
202 use super::*;
203
204 #[test]
205 fn test_macro_error_string() {
206 let error = error!("error").context("foo");
207 assert_eq!(error.to_string(), "foo\r\n ↪ error");
208 }
209
210 #[test]
211 fn test_macro_error_format_string() {
212 let error = error!("error {}", 404).context("foo");
213 assert_eq!(error.to_string(), "foo\r\n ↪ error 404");
214 }
215
216 #[derive(Debug)]
217 struct CustomError;
218
219 impl std::fmt::Display for CustomError {
220 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
221 write!(f, "entity not found")
222 }
223 }
224
225 impl std::error::Error for CustomError {}
226
227 #[test]
228 fn test_macro_error_from_error() {
229 let error = error!(CustomError).context("foo");
230 assert_eq!(error.to_string(), "foo\r\n ↪ entity not found");
231 }
232}