1use axum::{
4 http::StatusCode,
5 response::{IntoResponse, Response},
6 Json,
7};
8use serde::Serialize;
9use thiserror::Error;
10
11#[derive(Serialize, Debug, Clone)]
12pub struct BulkFieldError {
13 pub index: usize,
14 pub field: String,
15 pub message: String,
16}
17
18#[derive(Error, Debug)]
19pub enum ConfigError {
20 #[error("missing reference: {kind} id '{id}'")]
21 MissingReference { kind: &'static str, id: String },
22 #[error("invalid primary key: table {table_id} column {column}")]
23 InvalidPrimaryKey { table_id: String, column: String },
24 #[error("duplicate path segment: {0}")]
25 DuplicatePathSegment(String),
26 #[error("config load: {0}")]
27 Load(String),
28 #[error("validation: {0}")]
29 Validation(String),
30}
31
32#[derive(Error, Debug)]
33pub enum AppError {
34 #[error(transparent)]
35 Config(#[from] ConfigError),
36 #[error("not found: {0}")]
37 NotFound(String),
38 #[error("validation: {0}")]
39 Validation(String),
40 #[error("database: {0}")]
41 Db(#[from] sqlx::Error),
42 #[error("conflict: {0}")]
43 Conflict(String),
44 #[error("bad request: {0}")]
45 BadRequest(String),
46 #[error("storage: {0}")]
47 Storage(String),
48 #[error("unauthorized: {0}")]
49 Unauthorized(String),
50 #[error("bulk validation failed")]
51 BulkValidation(Vec<BulkFieldError>),
52}
53
54#[derive(Serialize)]
55pub struct ErrorBody {
56 pub error: ErrorDetail,
57}
58
59#[derive(Serialize)]
60pub struct ErrorDetail {
61 pub code: String,
62 pub message: String,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub details: Option<serde_json::Value>,
65}
66
67pub fn db_error_field(e: &AppError) -> Option<String> {
72 #[cfg(feature = "postgres")]
73 if let AppError::Db(sqlx::Error::Database(ref db_err)) = e {
74 if let Some(pg_err) = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>() {
75 if let Some(detail) = pg_err.detail() {
76 if let Some(start) = detail.find('(') {
77 if let Some(end) = detail[start + 1..].find(')') {
78 let field = &detail[start + 1..start + 1 + end];
79 if !field.is_empty() && !field.contains(',') {
80 return Some(field.trim().to_string());
81 }
82 }
83 }
84 }
85 }
86 }
87 #[cfg(not(feature = "postgres"))]
88 let _ = e;
89 None
90}
91
92pub fn db_error_message(e: &AppError, field: Option<&str>) -> String {
94 if let AppError::Db(sqlx::Error::Database(ref db_err)) = e {
95 match db_err.kind() {
96 sqlx::error::ErrorKind::UniqueViolation => {
97 return match field {
98 Some(f) => format!("{} already exists", f),
99 None => "duplicate value violates unique constraint".to_string(),
100 }
101 }
102 sqlx::error::ErrorKind::ForeignKeyViolation => {
103 return match field {
104 Some(f) => format!("{} references a non-existent record", f),
105 None => "foreign key constraint violation".to_string(),
106 }
107 }
108 sqlx::error::ErrorKind::NotNullViolation => {
109 return match field {
110 Some(f) => format!("{} cannot be null", f),
111 None => "not null constraint violation".to_string(),
112 }
113 }
114 sqlx::error::ErrorKind::CheckViolation => {
115 return "check constraint violation".to_string();
116 }
117 _ => {}
118 }
119 }
120 e.to_string()
121}
122
123impl IntoResponse for AppError {
124 fn into_response(self) -> Response {
125 if let AppError::BulkValidation(ref errors) = self {
126 let affected: std::collections::HashSet<usize> =
127 errors.iter().map(|e| e.index).collect();
128 let body = ErrorBody {
129 error: ErrorDetail {
130 code: "bulk_validation_error".to_string(),
131 message: format!("Validation failed for {} item(s)", affected.len()),
132 details: Some(serde_json::to_value(errors).unwrap_or(serde_json::Value::Null)),
133 },
134 };
135 return (StatusCode::UNPROCESSABLE_ENTITY, Json(body)).into_response();
136 }
137 let (status, code) = match &self {
138 AppError::Config(_) => (StatusCode::INTERNAL_SERVER_ERROR, "config_error"),
139 AppError::NotFound(_) => (StatusCode::NOT_FOUND, "not_found"),
140 AppError::Validation(_) => (StatusCode::UNPROCESSABLE_ENTITY, "validation_error"),
141 AppError::Db(e) => {
142 if let sqlx::Error::RowNotFound = e {
143 (StatusCode::NOT_FOUND, "not_found")
144 } else {
145 (StatusCode::INTERNAL_SERVER_ERROR, "database_error")
146 }
147 }
148 AppError::Conflict(_) => (StatusCode::CONFLICT, "conflict"),
149 AppError::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"),
150 AppError::Storage(_) => (StatusCode::INTERNAL_SERVER_ERROR, "storage_error"),
151 AppError::Unauthorized(_) => (StatusCode::UNAUTHORIZED, "unauthorized"),
152 AppError::BulkValidation(_) => unreachable!(),
153 };
154 let body = ErrorBody {
155 error: ErrorDetail {
156 code: code.to_string(),
157 message: self.to_string(),
158 details: None,
159 },
160 };
161 (status, Json(body)).into_response()
162 }
163}