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;