Skip to main content

ocre/
error.rs

1use axum::http::StatusCode;
2
3use crate::FieldError;
4
5/// `Result` with [`Error`] as the default error type, returned by Ocre APIs and handlers.
6///
7/// # Examples
8///
9/// ```
10/// fn parse_id(text: &str) -> ocre::Result<i64> {
11///     text.parse().map_err(|_| ocre::Error::bad_request("id must be a number"))
12/// }
13///
14/// assert_eq!(parse_id("7").unwrap(), 7);
15/// assert!(parse_id("x").is_err());
16/// ```
17pub type Result<T, E = Error> = std::result::Result<T, E>;
18
19/// Handler error: an HTTP status plus, for client errors, a message the user may see.
20///
21/// Internal details are logged to the Worker logs (Workers Logs), never
22/// sent to the client. With the `html` feature it renders as an HTML error
23/// page (validation errors listed as full messages); JSON endpoints return
24/// [`ApiError`](crate::ApiError), which converts from it with `?`:
25/// `{"error": {"status": 404, "message": "Not found"}}`. `?` also converts a
26/// [`worker::Error`] into [`Error::Internal`].
27///
28/// | Variant | Status | Message sent |
29/// |---|---|---|
30/// | [`NotFound`](Self::NotFound) | 404 | `Not found` |
31/// | [`BadRequest`](Self::BadRequest) | 400 | its message |
32/// | [`Unauthorized`](Self::Unauthorized) | 401 | `Unauthorized` |
33/// | [`Forbidden`](Self::Forbidden) | 403 | `Forbidden` |
34/// | [`Invalid`](Self::Invalid) | 422 | `Validation failed` plus the field errors |
35/// | [`Conflict`](Self::Conflict) | 409 | its message |
36/// | [`PayloadTooLarge`](Self::PayloadTooLarge) | 413 | its message |
37/// | [`TooManyRequests`](Self::TooManyRequests) | 429 | `Too many requests. Try again later.` |
38/// | [`Internal`](Self::Internal) | 500 | `Internal server error` (message logged) |
39///
40/// # Examples
41///
42/// ```
43/// use ocre::{Error, OptionExt, Result};
44///
45/// fn find(id: i64) -> Result<&'static str> {
46///     match id {
47///         0 => Err(Error::bad_request("id must be positive")),
48///         1 => Ok("First post"),
49///         _ => None.or_404(),
50///     }
51/// }
52///
53/// assert_eq!(find(1).unwrap(), "First post");
54/// assert!(matches!(find(2), Err(Error::NotFound)));
55/// assert_eq!(find(0).unwrap_err().to_string(), "bad request: id must be positive");
56/// ```
57#[derive(Debug)]
58pub enum Error {
59    /// 404 Not Found: the record or route does not exist.
60    NotFound,
61    /// 400 Bad Request, with a message shown to the user.
62    BadRequest(String),
63    /// 401 Unauthorized: missing or invalid credentials (password, session, token).
64    ///
65    /// JSON responses add `WWW-Authenticate: Bearer`.
66    Unauthorized,
67    /// 403 Forbidden: signed in, but not allowed to do this.
68    Forbidden,
69    /// 422 Unprocessable Entity: failed validations, one entry per field error.
70    ///
71    /// Built by [`Validator::finish`](crate::Validator::finish). JSON answers
72    /// group messages by field: `"fields": {"title": ["can't be blank"]}`.
73    Invalid(Vec<FieldError>),
74    /// 409 Conflict: the record changed since it was read (optimistic
75    /// locking with a `lock_version` column), or a unique key already
76    /// exists; the message is shown to the user.
77    ///
78    /// Generated `update` functions of models with a `lock_version` field
79    /// return it for a stale version (Rails' `ActiveRecord::StaleObjectError`).
80    Conflict(String),
81    /// 413 Payload Too Large: the request body is over a limit, with a message shown to the user.
82    ///
83    /// See [`storage::Multipart`](crate::storage::Multipart).
84    PayloadTooLarge(String),
85    /// 429 Too Many Requests: a rate limit was hit.
86    ///
87    /// Returned by [`security::rate_limit`](crate::security::rate_limit).
88    TooManyRequests,
89    /// 500 Internal Server Error; the message goes to the Worker logs only.
90    ///
91    /// Logged as `[ocre] <message>` when the response is built; the client
92    /// sees `Internal server error`.
93    Internal(String),
94}
95
96/// What a client may see of an [`Error`], plus the internal message of a 500 (never sent).
97pub(crate) struct Public {
98    pub status: StatusCode,
99    pub message: String,
100    pub fields: Vec<FieldError>,
101    pub internal: Option<String>,
102}
103
104/// Response extension of a 500 built from [`Error::Internal`]: `serve`
105/// logs and reports the message with the request's details, and in debug
106/// builds shows it on the development error page.
107#[derive(Debug, Clone)]
108pub(crate) struct InternalError(pub String);
109
110impl Public {
111    /// `{"title": ["can't be blank", ...], ...}`, the shape Rails APIs use.
112    pub fn fields_json(&self) -> serde_json::Value {
113        let mut map = serde_json::Map::new();
114        for field in &self.fields {
115            let messages = map.entry(field.field.clone()).or_insert_with(|| serde_json::Value::Array(vec![]));
116            messages.as_array_mut().expect("inserted as an array").push(field.message.clone().into());
117        }
118        serde_json::Value::Object(map)
119    }
120}
121
122impl Error {
123    /// Builds a 400 [`Error::BadRequest`] whose message is shown to the user.
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// let err = ocre::Error::bad_request("limit must be a number");
129    /// assert_eq!(err.to_string(), "bad request: limit must be a number");
130    /// ```
131    pub fn bad_request(message: impl Into<String>) -> Self {
132        Self::BadRequest(message.into())
133    }
134
135    /// Builds a 500 [`Error::Internal`] whose message is logged, never shown to the user.
136    ///
137    /// Name the fix in the message, as Ocre's own errors do.
138    ///
139    /// # Examples
140    ///
141    /// ```
142    /// let err = ocre::Error::internal("KV binding `CACHE` is missing");
143    /// assert!(matches!(&err, ocre::Error::Internal(message) if message.contains("CACHE")));
144    /// ```
145    pub fn internal(message: impl Into<String>) -> Self {
146        Self::Internal(message.into())
147    }
148
149    /// Whether the error says a unique value is already used: a "has
150    /// already been taken" validation error (generated models check unique
151    /// fields before writing), D1's `UNIQUE constraint failed` (two requests
152    /// raced past the check), or a [`Conflict`](Self::Conflict).
153    ///
154    /// [`Query::create_or_first`](crate::Query::create_or_first) uses it to
155    /// fall back to the existing row.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use ocre::{Error, FieldError};
161    ///
162    /// assert!(Error::Invalid(vec![FieldError::new("email", "has already been taken")]).is_taken());
163    /// assert!(Error::internal("D1_ERROR: UNIQUE constraint failed: users.email").is_taken());
164    /// assert!(!Error::Invalid(vec![FieldError::new("email", "can't be blank")]).is_taken());
165    /// assert!(!Error::NotFound.is_taken());
166    /// ```
167    pub fn is_taken(&self) -> bool {
168        match self {
169            Self::Invalid(fields) => fields.iter().any(|field| field.message == "has already been taken"),
170            Self::Internal(message) => message.contains("UNIQUE constraint failed"),
171            Self::Conflict(_) => true,
172            _ => false,
173        }
174    }
175
176    /// Client-safe form: internal details move to [`Public::internal`] and
177    /// are replaced by a generic message.
178    pub(crate) fn into_public(self) -> Public {
179        let mut internal = None;
180        let (status, message, fields) = match self {
181            Self::NotFound => (StatusCode::NOT_FOUND, "Not found".to_owned(), vec![]),
182            Self::BadRequest(message) => (StatusCode::BAD_REQUEST, message, vec![]),
183            Self::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized".to_owned(), vec![]),
184            Self::Forbidden => (StatusCode::FORBIDDEN, "Forbidden".to_owned(), vec![]),
185            Self::Invalid(fields) => (StatusCode::UNPROCESSABLE_ENTITY, "Validation failed".to_owned(), fields),
186            Self::PayloadTooLarge(message) => (StatusCode::PAYLOAD_TOO_LARGE, message, vec![]),
187            Self::Conflict(message) => (StatusCode::CONFLICT, message, vec![]),
188            Self::TooManyRequests => {
189                (StatusCode::TOO_MANY_REQUESTS, "Too many requests. Try again later.".to_owned(), vec![])
190            }
191            Self::Internal(message) => {
192                internal = Some(message);
193                (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_owned(), vec![])
194            }
195        };
196        Public { status, message, fields, internal }
197    }
198}
199
200/// Marks `response` with [`InternalError`] when it answers an
201/// [`Error::Internal`] (`internal` from [`Public::internal`]), for `serve` to report.
202pub(crate) fn mark(internal: Option<String>, response: &mut axum::response::Response) {
203    if let Some(message) = internal {
204        response.extensions_mut().insert(InternalError(message));
205    }
206}
207
208impl std::fmt::Display for Error {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        match self {
211            Self::NotFound => f.write_str("not found"),
212            Self::BadRequest(message) => write!(f, "bad request: {message}"),
213            Self::Unauthorized => f.write_str("unauthorized"),
214            Self::Forbidden => f.write_str("forbidden"),
215            Self::Invalid(fields) => {
216                let messages: Vec<String> = fields.iter().map(FieldError::full_message).collect();
217                write!(f, "invalid: {}", messages.join(", "))
218            }
219            Self::PayloadTooLarge(message) => write!(f, "payload too large: {message}"),
220            Self::Conflict(message) => write!(f, "conflict: {message}"),
221            Self::TooManyRequests => f.write_str("too many requests"),
222            Self::Internal(message) => write!(f, "internal error: {message}"),
223        }
224    }
225}
226
227impl std::error::Error for Error {}
228
229impl From<worker::Error> for Error {
230    fn from(err: worker::Error) -> Self {
231        Self::Internal(err.to_string())
232    }
233}
234
235/// Logs `[ocre] <message>` as an `error` line (Workers Logs in the
236/// dashboard; stderr in native unit tests), for failures outside a request's
237/// error reporting.
238pub(crate) fn log_internal(message: &str) {
239    crate::log::Logger::new().error(format_args!("[ocre] {message}"));
240}
241
242/// Extension for `Option`: `option.or_404()?` turns a missing record into a 404 response.
243///
244/// # Examples
245///
246/// ```
247/// use ocre::{Error, OptionExt};
248///
249/// assert_eq!(Some(3).or_404().unwrap(), 3);
250/// assert!(matches!(None::<i32>.or_404(), Err(Error::NotFound)));
251/// ```
252pub trait OptionExt<T> {
253    /// The value, or [`Error::NotFound`] (404) when `None`.
254    ///
255    /// # Errors
256    ///
257    /// [`Error::NotFound`] when the option is `None`.
258    fn or_404(self) -> Result<T>;
259}
260
261impl<T> OptionExt<T> for Option<T> {
262    fn or_404(self) -> Result<T> {
263        self.ok_or(Error::NotFound)
264    }
265}
266
267#[cfg(test)]
268#[path = "../tests/error.rs"]
269mod tests;