Skip to main content

loco_rs/
errors.rs

1//! # Application Error Handling
2
3use axum::{
4    extract::rejection::JsonRejection,
5    http::{
6        header::{InvalidHeaderName, InvalidHeaderValue},
7        method::InvalidMethod,
8        StatusCode,
9    },
10};
11use lettre::{address::AddressError, transport::smtp};
12
13use crate::{controller::ErrorDetail, depcheck, validation::ModelValidationErrors};
14
15/*
16backtrace principles:
17- use a plan warapper variant with no 'from' conversion
18- hand-code "From" conversion and force capture there with 'bt', which
19  will wrap and create backtrace only if RUST_BACKTRACE=1.
20costs:
21- when RUST_BACKTRACE is not set, we don't pay for the capture and we dont pay for printing.
22
23 */
24impl From<serde_json::Error> for Error {
25    fn from(val: serde_json::Error) -> Self {
26        Self::JSON(val).bt()
27    }
28}
29
30/// Application-wide error type.
31///
32/// Variants are grouped into two regions, in this fixed order:
33///
34/// 1. **Client-facing / API errors** — conditions that are meaningful to an
35///    API caller and are mapped, one by one, to a specific status code by
36///    `impl IntoResponse for Error` (see `controller::mod`). This includes
37///    [`Error::Model`], since [`crate::model::ModelError`] variants
38///    (`EntityNotFound`, `EntityAlreadyExists`, `Validation`) map to distinct
39///    client statuses.
40/// 2. **Internal / infrastructure errors** — everything else (I/O, DB,
41///    queue, template rendering, email, etc.). These do not leak details to
42///    callers and are deliberately collapsed to a generic `500 Internal
43///    Server Error` by the same `IntoResponse` impl, via explicit (not
44///    wildcard) match arms so the compiler forces every new variant to be
45///    classified into one of the two regions.
46///
47/// This grouping is purely organizational: variant names, `#[error(...)]`
48/// messages, `#[from]` conversions, and `#[cfg(...)]` gates are unchanged,
49/// so it is not a breaking change.
50#[derive(thiserror::Error, Debug)]
51#[non_exhaustive]
52pub enum Error {
53    // ─────────────────────────── Client-facing / API errors ───────────────────────────
54    #[error("{0}")]
55    Message(String),
56
57    // API
58    #[error("{0}")]
59    Unauthorized(String),
60
61    // API
62    #[error("not found")]
63    NotFound,
64
65    #[error("{0}")]
66    BadRequest(String),
67
68    #[error("")]
69    CustomError(StatusCode, ErrorDetail),
70
71    #[error("internal server error")]
72    InternalServerError,
73
74    #[error(transparent)]
75    JsonRejection(#[from] JsonRejection),
76
77    #[error(transparent)]
78    AxumFormRejection(#[from] axum::extract::rejection::FormRejection),
79
80    #[error(transparent)]
81    Validation(#[from] ModelValidationErrors),
82
83    #[cfg(feature = "with-db")]
84    // Model
85    #[error(transparent)]
86    Model(#[from] crate::model::ModelError),
87
88    // ─────────────────────────── Internal / infrastructure errors ─────────────────────
89    #[error("{inner}\n{backtrace}")]
90    WithBacktrace {
91        inner: Box<Self>,
92        backtrace: Box<std::backtrace::Backtrace>,
93    },
94
95    #[error(
96        "error while running worker: no queue provider populated in context. Did you configure \
97         BackgroundQueue and connection details in `queue` in your config file?"
98    )]
99    QueueProviderMissing,
100
101    #[error("task not found: '{0}'")]
102    TaskNotFound(String),
103
104    #[error(transparent)]
105    Scheduler(#[from] crate::scheduler::Error),
106
107    #[error(transparent)]
108    Axum(#[from] axum::http::Error),
109
110    #[error(transparent)]
111    Tera(#[from] tera::Error),
112
113    #[error(transparent)]
114    JSON(serde_json::Error),
115
116    #[error("cannot parse `{1}`: {0}")]
117    YAMLFile(#[source] serde_yaml::Error, String),
118
119    #[error(transparent)]
120    YAML(#[from] serde_yaml::Error),
121
122    #[error("Error sending email: '{0}'")]
123    EmailSender(#[from] lettre::error::Error),
124
125    #[error("Error sending email (smtp): '{0}'")]
126    Smtp(#[from] smtp::Error),
127
128    #[error("Worker error: {0}")]
129    Worker(String),
130
131    #[error(transparent)]
132    IO(#[from] std::io::Error),
133
134    #[cfg(feature = "with-db")]
135    #[error(transparent)]
136    DB(#[from] sea_orm::DbErr),
137
138    #[error(transparent)]
139    ParseAddress(#[from] AddressError),
140
141    #[error(transparent)]
142    InvalidHeaderValue(#[from] InvalidHeaderValue),
143
144    #[error(transparent)]
145    InvalidHeaderName(#[from] InvalidHeaderName),
146
147    #[error(transparent)]
148    InvalidMethod(#[from] InvalidMethod),
149
150    #[cfg(feature = "worker_redis")]
151    #[error(transparent)]
152    Redis(#[from] redis::RedisError),
153
154    #[cfg(feature = "worker")]
155    #[error(transparent)]
156    Sqlx(#[from] sqlx::Error),
157
158    #[error(transparent)]
159    Storage(#[from] crate::storage::StorageError),
160
161    #[error(transparent)]
162    Cache(#[from] crate::cache::CacheError),
163
164    #[cfg(debug_assertions)]
165    #[error(transparent)]
166    Generators(#[from] loco_gen::Error),
167
168    #[error(transparent)]
169    VersionCheck(#[from] depcheck::VersionCheckError),
170
171    #[error(transparent)]
172    Any(#[from] Box<dyn std::error::Error + Send + Sync>),
173}
174
175impl Error {
176    pub fn wrap(err: impl std::error::Error + Send + Sync + 'static) -> Self {
177        Self::Any(Box::new(err)) //.bt()
178    }
179
180    pub fn msg(err: impl std::error::Error + Send + Sync + 'static) -> Self {
181        Self::Message(err.to_string()) //.bt()
182    }
183    #[must_use]
184    pub fn string(s: &str) -> Self {
185        Self::Message(s.to_string())
186    }
187    #[must_use]
188    pub fn bt(self) -> Self {
189        let backtrace = std::backtrace::Backtrace::capture();
190        match backtrace.status() {
191            std::backtrace::BacktraceStatus::Disabled
192            | std::backtrace::BacktraceStatus::Unsupported => self,
193            _ => Self::WithBacktrace {
194                inner: Box::new(self),
195                backtrace: Box::new(backtrace),
196            },
197        }
198    }
199}