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}