Skip to main content

modo/error/
mod.rs

1//! # modo::error
2//!
3//! HTTP-aware error type for the modo web framework.
4//!
5//! [`Error`] carries an HTTP status code, a human-readable message, an optional
6//! structured details payload, an optional boxed source error, an optional
7//! static machine-readable `error_code`, and an optional i18n `locale_key`. It
8//! implements [`axum::response::IntoResponse`], so `async fn` handlers can
9//! return [`Result<T>`] and use `?` everywhere.
10//!
11//! ## Provides
12//!
13//! - [`Error`] — primary framework error type with status + message + optional
14//!   source / details / error_code / locale_key
15//! - [`Result`] — type alias for `std::result::Result<T, Error>`
16//! - [`HttpError`] — lightweight `Copy` enum of common HTTP error statuses; converts
17//!   into [`Error`] via `From<HttpError>`
18//! - Status-code constructors: [`Error::bad_request`], [`Error::unauthorized`],
19//!   [`Error::forbidden`], [`Error::not_found`], [`Error::conflict`],
20//!   [`Error::payload_too_large`], [`Error::unprocessable_entity`],
21//!   [`Error::too_many_requests`], [`Error::internal`], [`Error::bad_gateway`],
22//!   [`Error::gateway_timeout`]
23//! - General constructors: [`Error::new`], [`Error::with_source`],
24//!   [`Error::localized`], [`Error::lagged`]
25//! - Builder methods: [`Error::chain`], [`Error::with_code`],
26//!   [`Error::with_details`], [`Error::with_locale_key`]
27//! - Automatic [`From`] conversions into [`Error`]:
28//!   [`std::io::Error`] → 500, [`serde_json::Error`] → 400,
29//!   [`serde_yaml_ng::Error`] → 500
30//!
31//! ## Quick start
32//!
33//! ```rust
34//! use modo::error::{Error, Result};
35//!
36//! fn parse_id(raw: &str) -> Result<u64> {
37//!     // `?` propagates any error whose `From` impl maps into [`Error`].
38//!     // For domain checks, return one of the named status constructors.
39//!     let id: u64 = raw
40//!         .parse()
41//!         .map_err(|_| Error::bad_request("invalid id"))?;
42//!     if id == 0 {
43//!         return Err(Error::not_found("user not found"));
44//!     }
45//!     Ok(id)
46//! }
47//! ```
48//!
49//! ## Source drops on clone and response — use `error_code` for identity
50//!
51//! The boxed `source` field is `Box<dyn std::error::Error + Send + Sync>`, which
52//! is not `Clone`. Both [`Clone`] and the [`IntoResponse`](axum::response::IntoResponse)
53//! impl therefore drop the source. This means:
54//!
55//! - **Pre-response (inside a handler or middleware that still owns the
56//!   `Error`)** — use [`Error::source_as::<T>`](Error::source_as) to downcast
57//!   the source to a concrete type.
58//! - **Post-response (middleware that reads the error copy stored in response
59//!   extensions)** — the source is gone; use [`Error::error_code`] to recover
60//!   the identity of the error. Attach it up front with [`Error::with_code`].
61//!
62//! A typical pattern is `.chain(e).with_code(e.code())` so you keep both the
63//! causal source (inspectable pre-response) and the static code (stable
64//! post-response).
65//!
66//! ```rust
67//! use modo::error::Error;
68//!
69//! let err = Error::unauthorized("token expired").with_code("jwt:expired");
70//! assert_eq!(err.error_code(), Some("jwt:expired"));
71//! ```
72//!
73//! ## Usage in middleware and guards
74//!
75//! Middleware and route guards that need to short-circuit with an error should
76//! build an [`Error`] and call [`IntoResponse::into_response`](axum::response::IntoResponse::into_response)
77//! — never construct raw [`axum::response::Response`] values by hand. This
78//! ensures the JSON body shape and response-extension copy stay consistent
79//! across the framework.
80
81mod convert;
82mod core;
83mod http_error;
84
85pub(crate) use core::render_error_body;
86pub use core::{Error, Result};
87pub use http_error::HttpError;