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("forbidden: {0}")]
51 Forbidden(String),
52 #[error("bulk validation failed")]
53 BulkValidation(Vec<BulkFieldError>),
54}
55
56#[derive(Serialize)]
57pub struct ErrorBody {
58 pub error: ErrorDetail,
59}
60
61#[derive(Serialize)]
62pub struct ErrorDetail {
63 pub code: String,
64 pub message: String,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub details: Option<serde_json::Value>,
67}
68
69pub fn db_error_field(e: &AppError) -> Option<String> {
74 #[cfg(feature = "postgres")]
75 if let AppError::Db(sqlx::Error::Database(ref db_err)) = e {
76 if let Some(pg_err) = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>() {
77 if let Some(detail) = pg_err.detail() {
78 if let Some(start) = detail.find('(') {
79 if let Some(end) = detail[start + 1..].find(')') {
80 let field = &detail[start + 1..start + 1 + end];
81 if !field.is_empty() && !field.contains(',') {
82 return Some(field.trim().to_string());
83 }
84 }
85 }
86 }
87 }
88 }
89 #[cfg(not(feature = "postgres"))]
90 let _ = e;
91 None
92}
93
94pub fn db_error_message(e: &AppError, field: Option<&str>) -> String {
96 if let AppError::Db(sqlx::Error::Database(ref db_err)) = e {
97 match db_err.kind() {
98 sqlx::error::ErrorKind::UniqueViolation => {
99 return match field {
100 Some(f) => format!("{} already exists", f),
101 None => "duplicate value violates unique constraint".to_string(),
102 }
103 }
104 sqlx::error::ErrorKind::ForeignKeyViolation => {
105 return match field {
106 Some(f) => format!("{} references a non-existent record", f),
107 None => "foreign key constraint violation".to_string(),
108 }
109 }
110 sqlx::error::ErrorKind::NotNullViolation => {
111 return match field {
112 Some(f) => format!("{} cannot be null", f),
113 None => "not null constraint violation".to_string(),
114 }
115 }
116 sqlx::error::ErrorKind::CheckViolation => {
117 return "check constraint violation".to_string();
118 }
119 _ => {}
120 }
121 }
122 e.to_string()
123}
124
125impl IntoResponse for AppError {
126 fn into_response(self) -> Response {
127 if let AppError::BulkValidation(ref errors) = self {
128 let affected: std::collections::HashSet<usize> =
129 errors.iter().map(|e| e.index).collect();
130 let body = ErrorBody {
131 error: ErrorDetail {
132 code: "bulk_validation_error".to_string(),
133 message: format!("Validation failed for {} item(s)", affected.len()),
134 details: Some(serde_json::to_value(errors).unwrap_or(serde_json::Value::Null)),
135 },
136 };
137 return (StatusCode::UNPROCESSABLE_ENTITY, Json(body)).into_response();
138 }
139 let (status, code) = match &self {
140 AppError::Config(_) => (StatusCode::INTERNAL_SERVER_ERROR, "config_error"),
141 AppError::NotFound(_) => (StatusCode::NOT_FOUND, "not_found"),
142 AppError::Validation(_) => (StatusCode::UNPROCESSABLE_ENTITY, "validation_error"),
143 AppError::Db(e) => {
144 if let sqlx::Error::RowNotFound = e {
145 (StatusCode::NOT_FOUND, "not_found")
146 } else {
147 (StatusCode::INTERNAL_SERVER_ERROR, "database_error")
148 }
149 }
150 AppError::Conflict(_) => (StatusCode::CONFLICT, "conflict"),
151 AppError::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"),
152 AppError::Storage(_) => (StatusCode::INTERNAL_SERVER_ERROR, "storage_error"),
153 AppError::Unauthorized(_) => (StatusCode::UNAUTHORIZED, "unauthorized"),
154 AppError::Forbidden(_) => (StatusCode::FORBIDDEN, "forbidden"),
155 AppError::BulkValidation(_) => unreachable!(),
156 };
157 let body = ErrorBody {
158 error: ErrorDetail {
159 code: code.to_string(),
160 message: self.to_string(),
161 details: None,
162 },
163 };
164 (status, Json(body)).into_response()
165 }
166}