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 find_user(id: u64) -> Result<String> {
37//! if id == 0 {
38//! return Err(Error::not_found("user not found"));
39//! }
40//! Ok("Alice".to_string())
41//! }
42//! ```
43//!
44//! ## Source drops on clone and response — use `error_code` for identity
45//!
46//! The boxed `source` field is `Box<dyn std::error::Error + Send + Sync>`, which
47//! is not `Clone`. Both [`Clone`] and [`Error::into_response`] therefore drop
48//! the source. This means:
49//!
50//! - **Pre-response (inside a handler or middleware that still owns the
51//! `Error`)** — use [`Error::source_as::<T>`](Error::source_as) to downcast
52//! the source to a concrete type.
53//! - **Post-response (middleware that reads the error copy stored in response
54//! extensions)** — the source is gone; use [`Error::error_code`] to recover
55//! the identity of the error. Attach it up front with [`Error::with_code`].
56//!
57//! A typical pattern is `.chain(e).with_code(e.code())` so you keep both the
58//! causal source (inspectable pre-response) and the static code (stable
59//! post-response).
60//!
61//! ```rust
62//! use modo::error::Error;
63//!
64//! let err = Error::unauthorized("token expired").with_code("jwt:expired");
65//! assert_eq!(err.error_code(), Some("jwt:expired"));
66//! ```
67//!
68//! ## Usage in middleware and guards
69//!
70//! Middleware and route guards that need to short-circuit with an error should
71//! build an [`Error`] and call [`IntoResponse::into_response`](axum::response::IntoResponse::into_response)
72//! — never construct raw [`axum::response::Response`] values by hand. This
73//! ensures the JSON body shape and response-extension copy stay consistent
74//! across the framework.
75
76mod convert;
77mod core;
78mod http_error;
79
80pub(crate) use core::render_error_body;
81pub use core::{Error, Result};
82pub use http_error::HttpError;