1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use axum::Json;
4use serde_json::json;
5
6#[derive(Debug, thiserror::Error)]
8pub enum AppError {
9 #[error("{0}")]
10 NotFound(String),
11 #[error("{0}")]
12 BadRequest(String),
13 #[error("{0}")]
14 Conflict(String),
15 #[error("{0}")]
16 Unauthorized(String),
17 #[error("{0}")]
18 Forbidden(String),
19 #[error(transparent)]
20 Db(#[from] sqlx::Error),
21 #[error(transparent)]
22 Other(#[from] anyhow::Error),
23}
24
25pub type AppResult<T> = Result<T, AppError>;
26
27impl IntoResponse for AppError {
28 fn into_response(self) -> Response {
29 let (status, msg) = match self {
30 AppError::NotFound(m) => (StatusCode::NOT_FOUND, m),
31 AppError::BadRequest(m) => (StatusCode::BAD_REQUEST, m),
32 AppError::Conflict(m) => (StatusCode::CONFLICT, m),
33 AppError::Unauthorized(m) => (StatusCode::UNAUTHORIZED, m),
34 AppError::Forbidden(m) => (StatusCode::FORBIDDEN, m),
35 AppError::Db(sqlx::Error::RowNotFound) => {
36 (StatusCode::NOT_FOUND, "resource not found".to_string())
37 }
38 AppError::Db(sqlx::Error::PoolTimedOut) => (
41 StatusCode::SERVICE_UNAVAILABLE,
42 "database busy; retry shortly".to_string(),
43 ),
44 AppError::Db(sqlx::Error::Database(ref dbe)) => {
47 use sqlx::error::ErrorKind;
48 let busy = matches!(
52 dbe.code().as_deref(),
53 Some("5") | Some("261") | Some("517") | Some("773")
54 ) || {
55 let m = dbe.message().to_ascii_lowercase();
56 m.contains("database is locked") || m.contains("database is busy")
57 };
58 if busy {
59 (
60 StatusCode::SERVICE_UNAVAILABLE,
61 "database busy; retry shortly".to_string(),
62 )
63 } else {
64 match dbe.kind() {
65 ErrorKind::UniqueViolation => {
66 (StatusCode::CONFLICT, "resource already exists".to_string())
67 }
68 ErrorKind::ForeignKeyViolation => (
69 StatusCode::BAD_REQUEST,
70 "referenced resource does not exist (check site_id)".to_string(),
71 ),
72 _ => {
73 tracing::error!(error = %dbe, "database error");
74 (
75 StatusCode::INTERNAL_SERVER_ERROR,
76 "database error".to_string(),
77 )
78 }
79 }
80 }
81 }
82 AppError::Db(e) => {
83 tracing::error!(error = %e, "database error");
84 (
85 StatusCode::INTERNAL_SERVER_ERROR,
86 "database error".to_string(),
87 )
88 }
89 AppError::Other(e) => {
90 tracing::error!(error = ?e, "internal error");
91 (
92 StatusCode::INTERNAL_SERVER_ERROR,
93 "internal error".to_string(),
94 )
95 }
96 };
97 let mut resp = (status, Json(json!({ "error": msg }))).into_response();
98 if status == StatusCode::SERVICE_UNAVAILABLE {
100 resp.headers_mut().insert(
101 axum::http::header::RETRY_AFTER,
102 axum::http::HeaderValue::from_static("1"),
103 );
104 }
105 resp
106 }
107}