Skip to main content

rainier_support/
error.rs

1//! The framework error type.
2//!
3//! Every Rainier component fails with the same [`Error`], and every `Error`
4//! carries the two things the HTTP layer needs to render it without knowing
5//! where it came from: an [`ErrorKind`] that maps to a status code, and an
6//! optional structured `details` payload.
7//!
8//! That is what keeps the crate graph acyclic. `rainier-validation` cannot
9//! add a `Validation` variant to an enum in `rainier-support` (that would be a
10//! cycle), so instead it builds `Error::validation(details)` and the exception
11//! handler in `rainier-http` renders `details` as JSON. The same trick serves
12//! auth, authorization, and model-not-found — no component needs to know the
13//! others' error types.
14
15use std::fmt;
16
17/// The class of failure, which is also the HTTP status it renders as.
18///
19/// Each domain failure names its status (authentication → 401, validation
20/// → 422, …) so the "throw a domain error, get the right response" flow
21/// works without any component knowing the renderer.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ErrorKind {
24    /// 400 — the request was malformed.
25    BadRequest,
26    /// 401 — no (or invalid) credentials. Raised by guards.
27    Unauthenticated,
28    /// 403 — authenticated but not permitted. Raised by gates/policies.
29    Unauthorized,
30    /// 404 — no such route or model.
31    NotFound,
32    /// 405 — the route exists but not for this method.
33    MethodNotAllowed,
34    /// 409 — the request conflicts with current state.
35    Conflict,
36    /// 413 — the body exceeded the configured limit.
37    PayloadTooLarge,
38    /// 422 — the request was well-formed but failed validation.
39    Validation,
40    /// 429 — throttled.
41    TooManyRequests,
42    /// 500 — an unhandled internal failure.
43    Internal,
44    /// 503 — the app is down for maintenance or a dependency is unavailable.
45    ServiceUnavailable,
46    /// Any other status, set explicitly.
47    Status(u16),
48}
49
50impl ErrorKind {
51    /// The HTTP status this kind renders as.
52    pub fn status(self) -> u16 {
53        match self {
54            ErrorKind::BadRequest => 400,
55            ErrorKind::Unauthenticated => 401,
56            ErrorKind::Unauthorized => 403,
57            ErrorKind::NotFound => 404,
58            ErrorKind::MethodNotAllowed => 405,
59            ErrorKind::Conflict => 409,
60            ErrorKind::PayloadTooLarge => 413,
61            ErrorKind::Validation => 422,
62            ErrorKind::TooManyRequests => 429,
63            ErrorKind::Internal => 500,
64            ErrorKind::ServiceUnavailable => 503,
65            ErrorKind::Status(code) => code,
66        }
67    }
68
69    /// `true` for 5xx — the errors worth logging as faults rather than as
70    /// ordinary client mistakes.
71    pub fn is_server_error(self) -> bool {
72        self.status() >= 500
73    }
74}
75
76/// A framework error: a [`kind`](Error::kind), a human message, an optional
77/// structured `details` payload, and an optional underlying cause.
78#[derive(Debug)]
79pub struct Error {
80    kind: ErrorKind,
81    message: String,
82    details: Option<serde_json::Value>,
83    source: Option<anyhow::Error>,
84}
85
86impl Error {
87    /// Build an error of an explicit kind.
88    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
89        Self { kind, message: message.into(), details: None, source: None }
90    }
91
92    /// 500 — an internal failure.
93    pub fn internal(message: impl Into<String>) -> Self {
94        Self::new(ErrorKind::Internal, message)
95    }
96
97    /// 400 — a malformed request.
98    pub fn bad_request(message: impl Into<String>) -> Self {
99        Self::new(ErrorKind::BadRequest, message)
100    }
101
102    /// 401 — missing or invalid credentials.
103    pub fn unauthenticated(message: impl Into<String>) -> Self {
104        Self::new(ErrorKind::Unauthenticated, message)
105    }
106
107    /// 403 — authenticated but not permitted.
108    pub fn unauthorized(message: impl Into<String>) -> Self {
109        Self::new(ErrorKind::Unauthorized, message)
110    }
111
112    /// 404 — nothing matched.
113    pub fn not_found(message: impl Into<String>) -> Self {
114        Self::new(ErrorKind::NotFound, message)
115    }
116
117    /// 409 — conflicting state.
118    pub fn conflict(message: impl Into<String>) -> Self {
119        Self::new(ErrorKind::Conflict, message)
120    }
121
122    /// 408 — the request took longer than this service is prepared to wait.
123    ///
124    /// What a [`Timeout`](https://docs.rs/rainier-middleware) middleware
125    /// returns. `408` rather than `504`: the timeout is this service's own
126    /// decision about its own handler, not a report about something upstream.
127    pub fn request_timeout(message: impl Into<String>) -> Self {
128        Self::new(ErrorKind::Status(408), message)
129    }
130
131    /// 429 — throttled.
132    ///
133    /// The caller should also send a `retry-after`; nothing here can know what
134    /// to put in it.
135    pub fn too_many_requests(message: impl Into<String>) -> Self {
136        Self::new(ErrorKind::TooManyRequests, message)
137    }
138
139    /// 503 — a dependency is down, or the application is in maintenance.
140    ///
141    /// The honest answer when a health check cannot reach the database. A 500
142    /// says "this service is broken"; a 503 says "try again", which is what a
143    /// load balancer and a retrying client both need to hear.
144    pub fn service_unavailable(message: impl Into<String>) -> Self {
145        Self::new(ErrorKind::ServiceUnavailable, message)
146    }
147
148    /// 422 with a structured field-error payload — what a failed
149    /// `FormRequest` produces.
150    pub fn validation(details: serde_json::Value) -> Self {
151        Self::new(ErrorKind::Validation, "The given data was invalid.").with_details(details)
152    }
153
154    /// Attach a structured payload rendered alongside the message.
155    pub fn with_details(mut self, details: serde_json::Value) -> Self {
156        self.details = Some(details);
157        self
158    }
159
160    /// Attach the underlying cause.
161    pub fn with_source(mut self, source: impl Into<anyhow::Error>) -> Self {
162        self.source = Some(source.into());
163        self
164    }
165
166    /// Re-label the kind (and therefore the status) without losing the rest.
167    pub fn with_kind(mut self, kind: ErrorKind) -> Self {
168        self.kind = kind;
169        self
170    }
171
172    /// This error's kind.
173    pub fn kind(&self) -> ErrorKind {
174        self.kind
175    }
176
177    /// The HTTP status this error renders as.
178    pub fn status(&self) -> u16 {
179        self.kind.status()
180    }
181
182    /// The human-readable message.
183    pub fn message(&self) -> &str {
184        &self.message
185    }
186
187    /// The structured payload, if any.
188    pub fn details(&self) -> Option<&serde_json::Value> {
189        self.details.as_ref()
190    }
191
192    /// The underlying cause, if one was attached.
193    pub fn source_error(&self) -> Option<&anyhow::Error> {
194        self.source.as_ref()
195    }
196}
197
198impl fmt::Display for Error {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        write!(f, "{}", self.message)?;
201        if let Some(source) = &self.source {
202            write!(f, ": {source}")?;
203        }
204        Ok(())
205    }
206}
207
208impl std::error::Error for Error {
209    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
210        self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
211    }
212}
213
214// `anyhow::Error` is what rainier_orm fails with, so this conversion is the
215// seam between the ORM's errors and the framework's. A database failure is
216// internal until something upstream re-labels it (e.g. a repository turning a
217// missing row into `not_found`).
218impl From<anyhow::Error> for Error {
219    fn from(source: anyhow::Error) -> Self {
220        Self::internal(source.to_string()).with_source(source)
221    }
222}
223
224impl From<std::io::Error> for Error {
225    fn from(source: std::io::Error) -> Self {
226        let kind = match source.kind() {
227            std::io::ErrorKind::NotFound => ErrorKind::NotFound,
228            std::io::ErrorKind::PermissionDenied => ErrorKind::Unauthorized,
229            _ => ErrorKind::Internal,
230        };
231        Self::new(kind, source.to_string()).with_source(anyhow::Error::new(source))
232    }
233}
234
235impl From<serde_json::Error> for Error {
236    fn from(source: serde_json::Error) -> Self {
237        Self::bad_request(format!("malformed JSON: {source}"))
238            .with_source(anyhow::Error::new(source))
239    }
240}
241
242impl From<std::fmt::Error> for Error {
243    fn from(source: std::fmt::Error) -> Self {
244        Self::internal(source.to_string())
245    }
246}
247
248/// The framework result type.
249pub type Result<T, E = Error> = std::result::Result<T, E>;
250
251/// Adds `.context(..)` to any `Result` whose error converts into [`Error`],
252/// the way `anyhow::Context` does — but preserving the [`ErrorKind`] when the
253/// error already is a Rainier error, so adding context to a 404 does not
254/// silently turn it into a 500.
255pub trait Context<T> {
256    /// Prefix the error message with `context`.
257    fn context(self, context: impl fmt::Display) -> Result<T>;
258
259    /// Prefix the error message with a lazily built `context`.
260    fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T>;
261}
262
263impl<T, E> Context<T> for Result<T, E>
264where
265    E: Into<Error>,
266{
267    fn context(self, context: impl fmt::Display) -> Result<T> {
268        self.map_err(|e| {
269            let e = e.into();
270            let kind = e.kind();
271            Error::new(kind, format!("{context}: {e}"))
272        })
273    }
274
275    fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T> {
276        self.map_err(|e| {
277            let e = e.into();
278            let kind = e.kind();
279            Error::new(kind, format!("{}: {e}", context()))
280        })
281    }
282}
283
284impl<T> Context<T> for Option<T> {
285    fn context(self, context: impl fmt::Display) -> Result<T> {
286        self.ok_or_else(|| Error::internal(context.to_string()))
287    }
288
289    fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T> {
290        self.ok_or_else(|| Error::internal(context().to_string()))
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn kinds_map_to_statuses() {
300        assert_eq!(ErrorKind::Validation.status(), 422);
301        assert_eq!(ErrorKind::Unauthenticated.status(), 401);
302        assert_eq!(ErrorKind::Status(418).status(), 418);
303        assert!(ErrorKind::Internal.is_server_error());
304        assert!(!ErrorKind::NotFound.is_server_error());
305    }
306
307    #[test]
308    fn context_preserves_the_kind() {
309        let err: Result<()> = Err(Error::not_found("no such post"));
310        let err = err.context("loading the post").unwrap_err();
311        assert_eq!(err.kind(), ErrorKind::NotFound);
312        assert_eq!(err.message(), "loading the post: no such post");
313    }
314
315    #[test]
316    fn validation_errors_carry_their_details() {
317        let err = Error::validation(serde_json::json!({ "email": ["required"] }));
318        assert_eq!(err.status(), 422);
319        assert!(err.details().is_some());
320    }
321
322    #[test]
323    fn anyhow_errors_arrive_as_internal() {
324        let err: Error = anyhow::anyhow!("connection reset").into();
325        assert_eq!(err.status(), 500);
326        assert!(err.source_error().is_some());
327    }
328}