1use axum::http::StatusCode;
2use axum::response::{Html, IntoResponse, Response};
3
4pub type Result<T = (), E = Error> = std::result::Result<T, E>;
6
7#[non_exhaustive]
22pub enum Error {
23 BadRequest(String),
25 Unauthorized,
27 Forbidden,
29 NotFound,
31 PageExpired,
33 TooManyRequests,
35 ServiceUnavailable,
37 Validation(crate::validation::ValidationError),
39 Status(StatusCode, String),
42 Internal(anyhow::Error),
44}
45
46pub fn abort(status: StatusCode, message: impl Into<String>) -> Error {
49 Error::Status(status, message.into())
50}
51
52pub fn abort_if(condition: bool, status: StatusCode, message: impl Into<String>) -> Result {
54 if condition {
55 Err(abort(status, message))
56 } else {
57 Ok(())
58 }
59}
60
61pub fn abort_unless(condition: bool, status: StatusCode, message: impl Into<String>) -> Result {
63 abort_if(!condition, status, message)
64}
65
66#[derive(Debug)]
69struct Permanent(anyhow::Error);
70
71impl std::fmt::Display for Permanent {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 std::fmt::Display::fmt(&self.0, f)
74 }
75}
76
77impl std::error::Error for Permanent {
78 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
79 self.0.chain().nth(1)
80 }
81}
82
83impl Error {
84 pub fn permanent(err: impl Into<anyhow::Error>) -> Self {
87 Self::Internal(anyhow::Error::new(Permanent(err.into())))
88 }
89
90 pub fn permanent_message(message: impl std::fmt::Display) -> Self {
93 Self::permanent(anyhow::anyhow!("{message}"))
94 }
95
96 pub fn is_permanent(&self) -> bool {
98 match self {
99 Self::Internal(err) => err.chain().any(|e| e.is::<Permanent>()),
100 _ => false,
101 }
102 }
103
104 pub fn is_retryable(&self) -> bool {
107 match self {
108 Self::Internal(err) => err.chain().any(|e| {
109 e.downcast_ref::<crate::db::DbError>()
110 .is_some_and(crate::db::DbError::is_retryable)
111 }),
112 _ => false,
113 }
114 }
115
116 pub fn is_unique_violation(&self) -> bool {
119 match self {
120 Self::Internal(err) => err.chain().any(|e| {
121 e.downcast_ref::<crate::db::DbError>()
122 .is_some_and(crate::db::DbError::is_unique_violation)
123 || matches!(
124 e.downcast_ref::<sqlx::Error>(),
125 Some(sqlx::Error::Database(db)) if db.is_unique_violation()
126 )
127 }),
128 _ => false,
129 }
130 }
131
132 pub fn status(&self) -> StatusCode {
134 match self {
135 Self::BadRequest(_) => StatusCode::BAD_REQUEST,
136 Self::Unauthorized => StatusCode::UNAUTHORIZED,
137 Self::Forbidden => StatusCode::FORBIDDEN,
138 Self::NotFound => StatusCode::NOT_FOUND,
139 Self::PageExpired => StatusCode::from_u16(419).expect("valid status code"),
140 Self::TooManyRequests => StatusCode::TOO_MANY_REQUESTS,
141 Self::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE,
142 Self::Validation(_) => StatusCode::UNPROCESSABLE_ENTITY,
143 Self::Status(status, _) => *status,
144 Self::Internal(_) if self.is_unique_violation() => StatusCode::CONFLICT,
146 Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
147 }
148 }
149}
150
151impl std::fmt::Debug for Error {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 match self {
155 Self::Internal(err) => write!(f, "{err:?}"),
156 Self::BadRequest(msg) => write!(f, "bad request: {msg}"),
157 Self::Status(status, msg) => write!(f, "{}: {msg}", status.as_u16()),
158 Self::Validation(err) => write!(f, "validation failed: {:?}", err.errors),
159 other => write!(f, "{}", reason(other.status())),
160 }
161 }
162}
163
164impl std::fmt::Display for Error {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 match self {
181 Self::Internal(err) => write!(f, "{err}"),
182 Self::BadRequest(msg) | Self::Status(_, msg) => f.write_str(msg),
183 Self::Validation(err) => {
184 let mut first = true;
185 for (field, messages) in err.errors.iter() {
186 for message in messages {
187 if !first {
188 f.write_str("; ")?;
189 }
190 first = false;
191 write!(f, "{field}: {message}")?;
192 }
193 }
194 if first {
195 f.write_str(reason(self.status()))?;
196 }
197 Ok(())
198 }
199 other => f.write_str(reason(other.status())),
200 }
201 }
202}
203
204impl<E: Into<anyhow::Error>> From<E> for Error {
205 fn from(err: E) -> Self {
206 Self::Internal(err.into())
207 }
208}
209
210#[derive(Debug, Clone)]
213pub(crate) struct ErrorPage {
214 pub status: StatusCode,
215 pub detail: Option<String>,
217 pub debug_detail: Option<String>,
219 pub template: Option<String>,
222}
223
224impl ErrorPage {
225 pub fn shown_detail(&self, debug: bool) -> Option<&str> {
227 self.detail.as_deref().or(if debug {
228 self.debug_detail.as_deref()
229 } else {
230 None
231 })
232 }
233}
234
235impl ErrorPage {
236 pub fn json(&self, debug: bool) -> Response {
238 let message = self
239 .shown_detail(debug)
240 .unwrap_or_else(|| reason(self.status));
241 (
242 self.status,
243 axum::Json(serde_json::json!({ "message": message })),
244 )
245 .into_response()
246 }
247}
248
249pub(crate) fn wants_json(headers: &axum::http::HeaderMap) -> bool {
251 let header = |name| headers.get(name).and_then(|v| v.to_str().ok());
252 header(axum::http::header::ACCEPT).is_some_and(|v| v.contains("application/json"))
253 || header(axum::http::header::CONTENT_TYPE)
254 .is_some_and(|v| v.starts_with("application/json"))
255}
256
257pub(crate) fn panic_message(panic: &(dyn std::any::Any + Send)) -> String {
259 panic
260 .downcast_ref::<String>()
261 .cloned()
262 .or_else(|| panic.downcast_ref::<&str>().map(|s| (*s).to_owned()))
263 .unwrap_or_else(|| "(no message)".into())
264}
265
266pub(crate) fn reason(status: StatusCode) -> &'static str {
267 match status.as_u16() {
268 419 => "Page Expired",
269 _ => status.canonical_reason().unwrap_or("Error"),
270 }
271}
272
273impl From<crate::validation::ValidationError> for Error {
274 fn from(err: crate::validation::ValidationError) -> Self {
275 Self::Validation(err)
276 }
277}
278
279impl From<crate::validation::Errors> for Error {
280 fn from(errors: crate::validation::Errors) -> Self {
281 Self::Validation(errors.into())
282 }
283}
284
285impl IntoResponse for Error {
286 fn into_response(self) -> Response {
287 if let Self::Validation(err) = self {
288 return err.into_response();
289 }
290 let status = self.status();
291 let mut template = None;
292 let (detail, debug_detail) = match &self {
293 Self::Internal(err) => {
294 tracing::error!(error = ?err, "internal server error");
295 crate::report::request_error(err);
296 template = err
297 .chain()
298 .find_map(|e| e.downcast_ref::<minijinja::Error>())
299 .map(|e| e.display_debug_info().to_string())
300 .filter(|info| !info.trim().is_empty());
301 (None, Some(format!("{err:?}")))
302 }
303 Self::BadRequest(msg) => (Some(msg.clone()), None),
304 Self::Status(_, msg) if !msg.is_empty() => (Some(msg.clone()), None),
305 _ => (None, None),
306 };
307 let mut res = (status, Html(error_page(status, detail.as_deref()))).into_response();
310 res.extensions_mut().insert(ErrorPage {
311 status,
312 detail,
313 debug_detail,
314 template,
315 });
316 res
317 }
318}
319
320pub(crate) fn error_page(status: StatusCode, detail: Option<&str>) -> String {
322 let code = status.as_u16();
323 let reason = reason(status);
324 let detail = detail
325 .map(|d| format!("<pre>{}</pre>", escape(d)))
326 .unwrap_or_default();
327 format!(
328 "<!doctype html><html><head><meta charset=\"utf-8\">\
329 <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\
330 <title>{code} {reason}</title></head>\
331 <body><h1>{code} · {reason}</h1>{detail}</body></html>"
332 )
333}
334
335fn escape(s: &str) -> String {
336 s.replace('&', "&")
337 .replace('<', "<")
338 .replace('>', ">")
339 .replace('"', """)
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn validation_errors_answer_422_and_other_errors_500() {
348 let mut errors = crate::validation::Errors::new();
349 errors.add("name", "The name field is required.");
350 let err = Error::Validation(crate::validation::ValidationError::new(errors));
351 assert_eq!(err.status(), StatusCode::UNPROCESSABLE_ENTITY);
352 let plain = Error::from(anyhow::anyhow!("no database involved"));
353 assert!(!plain.is_unique_violation());
354 assert_eq!(plain.status(), StatusCode::INTERNAL_SERVER_ERROR);
355 }
356
357 #[tokio::test]
360 async fn a_raw_sqlx_unique_violation_is_a_conflict() {
361 let db = crate::db::connect(&crate::Config::default()).await.unwrap();
362 let Some(pool) = db.sqlite() else {
363 return; };
365 sqlx::raw_sql("CREATE TABLE tags (name TEXT UNIQUE); INSERT INTO tags VALUES ('a')")
366 .execute(pool)
367 .await
368 .unwrap();
369 let raw = sqlx::raw_sql("INSERT INTO tags VALUES ('a')")
370 .execute(pool)
371 .await
372 .unwrap_err();
373 let err = Error::from(anyhow::Error::new(raw));
374 assert!(err.is_unique_violation());
375 assert_eq!(err.status(), StatusCode::CONFLICT);
376 }
377}