Skip to main content

axum_ctx/
lib.rs

1//! Axum error handling inspired by [`anyhow`](https://docs.rs/anyhow)
2//!
3//! ## Comparison to [`anyhow`](https://docs.rs/anyhow)
4//!
5//! Assume a function `can_fail` that returns `Result<T, E>` or `Option<T>`.
6//!
7//! With `anyhow`, you can do the following:
8//!
9//! ```rust
10//! use anyhow::{Context, Result};
11//!
12//! # fn can_fail() -> Option<()> {
13//! #     None
14//! # }
15//! #
16//! # fn example() -> Result<()> {
17//! let value = can_fail().context("Error message")?;
18//! # Ok(())
19//! # }
20//! ```
21//!
22//! For many types of programs, this is more than enough.
23//! But for web backends, you don't only want to report an error.
24//! You want to return a response with a proper HTTP status code.
25//! Then you want to log the error (using [`tracing`]).
26//! This is what `axum-ctx` does:
27//!
28//! ```rust
29//! // Use a wildcard for the best user experience
30//! use axum_ctx::*;
31//!
32//! # fn can_fail() -> Option<()> {
33//! #     None
34//! # }
35//! #
36//! # fn example() -> RespResult<()> {
37//! let value = can_fail().ctx(StatusCode::BAD_REQUEST).log_msg("Error message")?;
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! If an error occurs, the user gets the error message "400 Bad Request" corresponding to the status code that you specified.
43//! But you can replace this default message with a custom error message to be shown to the user:
44//!
45//! ```rust
46//! # use axum_ctx::*;
47//! #
48//! # fn can_fail() -> Option<()> {
49//! #     None
50//! # }
51//! #
52//! # fn example() -> RespResult<()> {
53//! let value = can_fail()
54//!     .ctx(StatusCode::UNAUTHORIZED)
55//!     // Shown to the user
56//!     .user_msg("You are not allowed to access this resource!")
57//!     // NOT shown to the user, only for the log
58//!     .log_msg("Someone tries to pentest you")?;
59//! # Ok(())
60//! # }
61//! ```
62//!
63//! A second call of `user_msg` replaces the the user error message.
64//! But calling `log_msg` multiple times creates a backtrace:
65//!
66//! ```rust
67//! # use axum_ctx::*;
68//! #
69//! # fn can_fail() -> Option<()> {
70//! #     None
71//! # }
72//! #
73//! # fn example() -> RespResult<()> {
74//! fn returns_resp_result() -> RespResult<()> {
75//!     can_fail().ctx(StatusCode::NOT_FOUND).log_msg("Inner error message")
76//! }
77//!
78//! let value = returns_resp_result()
79//!     .log_msg("Outer error message")?;
80//! # Ok(())
81//! # }
82//! ```
83//!
84//! The code above leads to the following log message:
85//!
86//! ```text
87//! 2024-05-08T22:17:53.769240Z  INFO axum_ctx: 404 Not Found
88//!   0: Outer error message
89//!   1: Inner error message
90//! ```
91//!
92//! ## Lazy evaluation
93//! Similar to [`with_context`](https://docs.rs/anyhow/1.0.83/anyhow/trait.Context.html#tymethod.with_context) provided by `anyhow`, `axum-ctx` also supports lazy evaluation of [messages](Message).
94//! You just provide a closure to `user_msg` or `log_msg`:
95//!
96//! ```rust
97//! # use axum_ctx::*;
98//! #
99//! # fn can_fail() -> Option<()> {
100//! #     None
101//! # }
102//! #
103//! # fn example() -> RespResult<()> {
104//! let resource_name = "foo";
105//! let value = can_fail()
106//!     .ctx(StatusCode::UNAUTHORIZED)
107//!     .user_msg(|| format!("You are not allowed to access the resource {resource_name}!"))
108//!     .log_msg(|| format!("Someone tries to access {resource_name}"))?;
109//! # Ok(())
110//! # }
111//! ```
112//!
113//! `.user_msg(format!("…"))` creates the string on the heap even if `can_fail` didn't return `Err` (or `None` for options).
114//! `.user_msg(|| format!("…"))` (a closure with two pipes `||`) only creates the string if `Err`/`None` actually occurred.
115//!
116//! ## Logging
117//!
118//! `axum-ctx` uses [`tracing`] for logging.
119//! This means that you need to [initialize a tracing subscriber](https://docs.rs/tracing-subscriber/0.3.18/tracing_subscriber/fmt/index.html) in your program first before being able to see the log messages of `axum-ctx`.
120//!
121//! `axum-ctx` automatically chooses a [tracing level](tracing::Level) depending on the chosen status code.
122//! Here is the default range mapping (status codes less than 100 or bigger than 999 are not allowed):
123//!
124//! | Status Code  | Level   |
125//! | ------------ | ------- |
126//! | `100..400`   | `Debug` |
127//! | `400..500`   | `Info`  |
128//! | `500..600`   | `Error` |
129//! | `600..1000`  | `Trace` |
130//!
131//! You can change the default level for one or more status codes using [`change_tracing_level`] on program initialization
132//!
133//! ## Example
134//!
135//! Assume that you want to get all salaries from a database and then return their maximum from an Axum API.
136//!
137//! The steps required:
138//!
139//! > **1.** Get all salaries from the database. This might fail for example if the database isn't reachable
140//! >
141//! > ➡️ You need to handle a `Result`
142//! >
143//! > **2.** Determine the maximum salary. But if there were no salaries in the database, there is no maximum
144//! >
145//! > ➡️ You need to handle an `Option`
146//! >
147//! > **3.** Return the maximum salary as JSON.
148//!
149//! First, let's define a function to get all salaries:
150//!
151//! ```rust
152//! async fn salaries_from_db() -> Result<Vec<f64>, String> {
153//!     // Imagine getting this error while trying to connect to the database.
154//!     Err(String::from("Database unreachable"))
155//! }
156//! ```
157//!
158//! Now, let's see how to do proper handling of `Result` and `Option` in an Axum handler:
159//!
160//! ```rust
161//! use axum::Json;
162//! use http::StatusCode;
163//! use tracing::{error, info};
164//!
165//! # async fn salaries_from_db() -> Result<Vec<f64>, String> {
166//! #     // Imagine getting this error while trying to connect to the database.
167//! #     Err(String::from("Database unreachable"))
168//! # }
169//! #
170//! async fn max_salary() -> Result<Json<f64>, (StatusCode, &'static str)> {
171//!     let salaries = match salaries_from_db().await {
172//!         Ok(salaries) => salaries,
173//!         Err(error) => {
174//!             error!("Failed to get all salaries from the DB\n{error}");
175//!             return Err((
176//!                 StatusCode::INTERNAL_SERVER_ERROR,
177//!                 "Something went wrong. Please try again later",
178//!             ));
179//!         }
180//!     };
181//!
182//!     match salaries.iter().copied().reduce(f64::max) {
183//!         Some(max_salary) => Ok(Json(max_salary)),
184//!         None => {
185//!             info!("The maximum salary was requested although there are no salaries");
186//!             Err((StatusCode::NOT_FOUND, "There are no salaries yet!"))
187//!         }
188//!     }
189//! }
190//! ```
191//!
192//! Now, compare the code above with the one below that uses `axum-ctx`:
193//!
194//! ```rust
195//! # use axum::Json;
196//! use axum_ctx::*;
197//! # use tracing::{error, info};
198//!
199//! # async fn salaries_from_db() -> Result<Vec<f64>, String> {
200//! #     // Imagine getting this error while trying to connect to the database.
201//! #     Err(String::from("Database unreachable"))
202//! # }
203//! #
204//! async fn max_salary() -> RespResult<Json<f64>> {
205//!     salaries_from_db()
206//!         .await
207//!         .ctx(StatusCode::INTERNAL_SERVER_ERROR)
208//!         .user_msg("Something went wrong. Please try again later")
209//!         .log_msg("Failed to get all salaries from the DB")?
210//!         .iter()
211//!         .copied()
212//!         .reduce(f64::max)
213//!         .ctx(StatusCode::NOT_FOUND)
214//!         .user_msg("There are no salaries yet!")
215//!         .log_msg("The maximum salary was requested although there are no salaries")
216//!         .map(Json)
217//! }
218//! ```
219//!
220//! Isn't that a wonderful chain? ⛓️ It is basically a "one-liner" if you ignore the pretty formatting.
221//!
222//! The user gets the message "Something went wrong. Please try again later". In your terminal, you get the following log message:
223//!
224//! ```text
225//! 2024-05-08T22:17:53.769240Z  ERROR axum_ctx: Something went wrong. Please try again later
226//!   0: Failed to get all salaries from the DB
227//!   1: Database unreachable
228//! ```
229//!
230//! "What about `map_or_else` and `ok_or_else`?", you might ask.
231//! You can use them if you prefer chaining like me, but the code will not be as concise as the one above with `axum_ctx`.
232//! You can compare:
233//!
234//! ```rust
235//! # use axum::Json;
236//! # use http::StatusCode;
237//! # use tracing::{error, info};
238//! #
239//! # async fn salaries_from_db() -> Result<Vec<f64>, String> {
240//! #     // Imagine getting this error while trying to connect to the database.
241//! #     Err(String::from("Database unreachable"))
242//! # }
243//! #
244//! async fn max_salary() -> Result<Json<f64>, (StatusCode, &'static str)> {
245//!     salaries_from_db()
246//!         .await
247//!         .map_err(|error| {
248//!             error!("Failed to get all salaries from the DB\n{error}");
249//!             (
250//!                 StatusCode::INTERNAL_SERVER_ERROR,
251//!                 "Something went wrong. Please try again later",
252//!             )
253//!         })?
254//!         .iter()
255//!         .copied()
256//!         .reduce(f64::max)
257//!         .ok_or_else(|| {
258//!             info!("The maximum salary was requested although there are no salaries");
259//!             (StatusCode::NOT_FOUND, "There are no salaries yet!")
260//!         })
261//!         .map(Json)
262//! }
263//! ```
264
265use axum_core::response::{IntoResponse, Response};
266use std::{
267    borrow::Cow,
268    fmt,
269    sync::atomic::{AtomicU8, Ordering},
270};
271use tracing::{event, Level};
272
273pub use http::StatusCode;
274
275static STATUS_CODE_TRACE_LEVEL: [AtomicU8; 1000] = {
276    let mut array = [const { AtomicU8::new(TracingLevel::Trace as u8) }; 1000];
277
278    let mut ind = 100;
279    while ind < 400 {
280        array[ind] = AtomicU8::new(TracingLevel::Debug as u8);
281        ind += 1;
282    }
283    while ind < 500 {
284        array[ind] = AtomicU8::new(TracingLevel::Info as u8);
285        ind += 1;
286    }
287    while ind < 600 {
288        array[ind] = AtomicU8::new(TracingLevel::Error as u8);
289        ind += 1;
290    }
291
292    array
293};
294
295/// [`Result`] with [`RespErr`] as the error variant.
296pub type RespResult<T> = Result<T, RespErr>;
297
298/// The tracing level that maps to [`tracing::Level`].
299#[derive(Copy, Clone, PartialEq, Eq, Debug)]
300pub enum TracingLevel {
301    Trace,
302    Debug,
303    Info,
304    Warn,
305    Error,
306}
307
308/// Change the default tracing level for a status code.
309///
310/// Should only be used on program initialization.
311///
312/// # Panics
313/// Panics if the status code is less than 100 or greater than 999.
314///
315/// # Examples
316/// ```
317/// # use axum_ctx::{change_tracing_level, TracingLevel};
318/// for status_code in 100..200 {
319///     change_tracing_level(status_code, TracingLevel::Info);
320/// }
321/// ```
322///
323/// Examples of panics:
324///
325/// ```should_panic
326/// # use axum_ctx::{change_tracing_level, TracingLevel};
327/// change_tracing_level(99, TracingLevel::Info); // Less than 100
328/// ```
329///
330/// ```should_panic
331/// # use axum_ctx::{change_tracing_level, TracingLevel};
332/// change_tracing_level(1000, TracingLevel::Info); // Greater than 999
333/// ```
334pub fn change_tracing_level(status_code: usize, level: TracingLevel) {
335    assert!(
336        (100..1000).contains(&status_code),
337        "The status code has to be >=100 and <1000",
338    );
339
340    STATUS_CODE_TRACE_LEVEL[status_code].store(level as u8, Ordering::Relaxed);
341}
342
343/// An error message.
344#[derive(Debug)]
345pub struct Message(pub Cow<'static, str>);
346
347impl From<&'static str> for Message {
348    fn from(value: &'static str) -> Self {
349        Self(Cow::Borrowed(value))
350    }
351}
352
353impl From<String> for Message {
354    fn from(value: String) -> Self {
355        Self(Cow::Owned(value))
356    }
357}
358
359impl<F, V> From<F> for Message
360where
361    F: FnOnce() -> V,
362    V: Into<Self>,
363{
364    fn from(f: F) -> Self {
365        f().into()
366    }
367}
368
369#[derive(Debug)]
370enum ResponseKind {
371    /// Shows a default message to the user.
372    DefaultMessage,
373    /// Shows a custom message to the user.
374    CustomMessage(Message),
375    /// A custom response.
376    Response(Response),
377}
378
379/// An error to be used as the error variant of a request handler.
380///
381/// Often initialized by using [`RespErrCtx::ctx`] on [`Result`], [`Option`] or [`Response`].
382/// But it can also be initialized by [`RespErr::new`].
383///
384/// # Examples
385///
386/// ```
387/// use axum_ctx::*;
388///
389/// fn can_fail() -> Result<(), std::io::Error> {
390///     // …
391///     # Ok(())
392/// }
393///
394/// async fn get() -> Result<StatusCode, RespErr> {
395///     can_fail()
396///         .ctx(StatusCode::INTERNAL_SERVER_ERROR)
397///         .user_msg("Sorry for disappointing you. Do you want a cookie?")
398///         .log_msg("Failed to do …. Blame Max Mustermann")?;
399///     // …
400///     Ok(StatusCode::OK)
401/// }
402/// ```
403///
404/// Using the type alias [`RespResult`] is recommended:
405///
406/// ```
407/// # use axum_ctx::*;
408/// async fn get() -> RespResult<StatusCode> {
409///     // …
410///     # Ok(StatusCode::OK)
411/// }
412/// ```
413#[derive(Debug)]
414pub struct RespErr {
415    pub status_code: StatusCode,
416    log_messages: Vec<Message>,
417    response_kind: ResponseKind,
418}
419
420impl RespErr {
421    /// Initialize with a status.
422    #[must_use]
423    pub const fn new(status_code: StatusCode) -> Self {
424        Self {
425            status_code,
426            log_messages: Vec::new(),
427            response_kind: ResponseKind::DefaultMessage,
428        }
429    }
430
431    /// Optionally add a custom user error message.
432    #[must_use]
433    pub fn user_msg(mut self, message: impl Into<Message>) -> Self {
434        self.response_kind = ResponseKind::CustomMessage(message.into());
435
436        self
437    }
438
439    /// Optionally add an error message to be showed in the log.
440    /// It will not be shown to the user!
441    #[must_use]
442    pub fn log_msg(mut self, error: impl Into<Message>) -> Self {
443        self.log_messages.push(error.into());
444
445        self
446    }
447}
448
449impl fmt::Display for RespErr {
450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451        match &self.response_kind {
452            ResponseKind::DefaultMessage => self.status_code.fmt(f)?,
453            ResponseKind::CustomMessage(message) => f.write_str(&message.0)?,
454            ResponseKind::Response(..) => (),
455        }
456
457        for (ind, e) in self.log_messages.iter().rev().enumerate() {
458            f.write_fmt(format_args!("\n  {ind}: {}", e.0))?;
459        }
460
461        Ok(())
462    }
463}
464
465impl IntoResponse for RespErr {
466    /// Log the error, set the HTTP status code and return the response.
467    fn into_response(self) -> Response {
468        let ind = usize::from(self.status_code.as_u16());
469
470        if let Some(tracing_level_u8) = STATUS_CODE_TRACE_LEVEL.get(ind) {
471            match tracing_level_u8.load(Ordering::Relaxed) {
472                0 => event!(Level::TRACE, "{self}"),
473                1 => event!(Level::DEBUG, "{self}"),
474                2 => event!(Level::INFO, "{self}"),
475                3 => event!(Level::WARN, "{self}"),
476                _ => event!(Level::ERROR, "{self}"),
477            }
478        }
479
480        let mut response = match self.response_kind {
481            ResponseKind::DefaultMessage => self.status_code.to_string().into_response(),
482            ResponseKind::CustomMessage(message) => message.0.into_response(),
483            ResponseKind::Response(r) => r,
484        };
485
486        *response.status_mut() = self.status_code;
487
488        response
489    }
490}
491
492/// Conversion to a `Result` with [`RespErr`] as the error.
493///
494/// Inspired by `anyhow::Context`, especially the conversion from [`Result<T, E>`](Result) or [`Option<T>`](Option) to `Result<T, RespErr>`.
495///
496/// After this conversion, you can add a user and/or error message using [`RespErrExt`].
497pub trait RespErrCtx<T> {
498    /// Convert by adding a status as a context.
499    fn ctx(self, status_code: StatusCode) -> Result<T, RespErr>;
500}
501
502impl<T, E> RespErrCtx<T> for Result<T, E>
503where
504    E: fmt::Display,
505{
506    /// The error is used as a log error message.
507    fn ctx(self, status_code: StatusCode) -> Result<T, RespErr> {
508        match self {
509            Ok(t) => Ok(t),
510            Err(e) => Err(RespErr::new(status_code).log_msg(e.to_string())),
511        }
512    }
513}
514
515impl<T> RespErrCtx<T> for Option<T> {
516    #[inline]
517    fn ctx(self, status_code: StatusCode) -> Result<T, RespErr> {
518        match self {
519            Some(v) => Ok(v),
520            None => Err(RespErr::new(status_code)),
521        }
522    }
523}
524
525impl<T> RespErrCtx<T> for Response {
526    fn ctx(self, status_code: StatusCode) -> Result<T, RespErr> {
527        Err(RespErr {
528            status_code,
529            log_messages: Vec::new(),
530            response_kind: ResponseKind::Response(self),
531        })
532    }
533}
534
535/// Addition of custom user and log error messages to a `Result<T, RespErr>`.
536pub trait RespErrExt<T> {
537    /// Add a custom user error message.
538    ///
539    /// See [`RespErr::user_msg`](crate::RespErr::user_msg).
540    fn user_msg(self, message: impl Into<Message>) -> Result<T, RespErr>;
541
542    /// Add a log error message.
543    ///
544    /// See [`RespErr::log_msg`](crate::RespErr::log_msg).
545    fn log_msg(self, error: impl Into<Message>) -> Result<T, RespErr>;
546}
547
548impl<T> RespErrExt<T> for Result<T, RespErr> {
549    #[inline]
550    fn user_msg(self, message: impl Into<Message>) -> Self {
551        match self {
552            Ok(t) => Ok(t),
553            Err(e) => Err(e.user_msg(message)),
554        }
555    }
556
557    #[inline]
558    fn log_msg(self, error: impl Into<Message>) -> Self {
559        match self {
560            Ok(t) => Ok(t),
561            Err(e) => Err(e.log_msg(error)),
562        }
563    }
564}