1use axum::{
7 Json,
8 http::StatusCode,
9 response::{IntoResponse, Response},
10};
11use serde_json::json;
12use thiserror::Error;
13
14#[derive(Error, Debug)]
16pub enum AppError {
17 #[error("Database error: {0}")]
19 Database(#[from] sqlx::Error),
20
21 #[error("Resource not found: {0}")]
23 NotFound(String),
24
25 #[error("Validation error: {0}")]
27 Validation(String),
28
29 #[error("Authentication error: {0}")]
31 Auth(String),
32
33 #[error("Authorization error: {0}")]
35 Forbidden(String),
36
37 #[error("Conflict: {0}")]
39 Conflict(String),
40
41 #[error("Bad request: {0}")]
43 BadRequest(String),
44
45 #[error("Internal error: {0}")]
47 Internal(String),
48
49 #[error("Configuration error: {0}")]
51 Config(String),
52
53 #[error("NATS error: {0}")]
55 Nats(String),
56
57 #[error("Serialization error: {0}")]
59 Serialization(#[from] serde_json::Error),
60
61 #[error("Template error: {0}")]
63 Template(String),
64
65 #[error("Encryption error: {0}")]
67 Encryption(String),
68
69 #[error("External service error: {0}")]
71 ExternalService(String),
72
73 #[error("Parse error: {0}")]
75 Parse(String),
76
77 #[error(
84 "Residency violation: credential '{credential}' is region-locked to '{entry_region}'; this server is in '{server_region}'"
85 )]
86 ResidencyViolation {
87 credential: String,
88 entry_region: String,
89 server_region: String,
90 },
91
92 #[error("Cross-region broker {broker_url} unreachable: {cause}")]
102 CrossRegionUnreachable { broker_url: String, cause: String },
103}
104
105impl IntoResponse for AppError {
106 fn into_response(self) -> Response {
107 let (status, error_message) = match &self {
108 AppError::Database(e) => {
109 tracing::error!(error = %e, "Database error");
110 (StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
111 }
112 AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
113 AppError::Validation(msg) => (StatusCode::UNPROCESSABLE_ENTITY, msg.clone()),
114 AppError::Auth(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
115 AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg.clone()),
116 AppError::Conflict(msg) => (StatusCode::CONFLICT, msg.clone()),
117 AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
118 AppError::Internal(msg) => {
119 tracing::error!(error = %msg, "Internal error");
120 (StatusCode::INTERNAL_SERVER_ERROR, msg.clone())
121 }
122 AppError::Config(msg) => {
123 tracing::error!(error = %msg, "Configuration error");
124 (StatusCode::INTERNAL_SERVER_ERROR, msg.clone())
125 }
126 AppError::Nats(msg) => {
127 tracing::error!(error = %msg, "NATS error");
128 (StatusCode::SERVICE_UNAVAILABLE, msg.clone())
129 }
130 AppError::Serialization(e) => {
131 tracing::error!(error = %e, "Serialization error");
132 (StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
133 }
134 AppError::Template(msg) => {
135 tracing::error!(error = %msg, "Template error");
136 (StatusCode::INTERNAL_SERVER_ERROR, msg.clone())
137 }
138 AppError::Encryption(msg) => {
139 tracing::error!(error = %msg, "Encryption error");
140 (StatusCode::INTERNAL_SERVER_ERROR, msg.clone())
141 }
142 AppError::ExternalService(msg) => {
143 tracing::warn!(error = %msg, "External service error");
144 (StatusCode::BAD_GATEWAY, msg.clone())
145 }
146 AppError::Parse(msg) => {
147 tracing::error!(error = %msg, "Parse error");
148 (StatusCode::BAD_REQUEST, msg.clone())
149 }
150 AppError::ResidencyViolation { .. } => {
151 tracing::warn!(error = %self, "Residency violation");
152 (StatusCode::FORBIDDEN, self.to_string())
153 }
154 AppError::CrossRegionUnreachable { .. } => {
155 tracing::warn!(error = %self, "Cross-region broker unreachable");
156 (StatusCode::BAD_GATEWAY, self.to_string())
157 }
158 };
159
160 let body = Json(json!({
161 "error": error_message,
162 "status": status.as_u16()
163 }));
164
165 (status, body).into_response()
166 }
167}
168
169pub type AppResult<T> = Result<T, AppError>;
171
172impl From<anyhow::Error> for AppError {
173 fn from(err: anyhow::Error) -> Self {
174 AppError::Internal(err.to_string())
175 }
176}
177
178impl From<envy::Error> for AppError {
179 fn from(err: envy::Error) -> Self {
180 AppError::Config(err.to_string())
181 }
182}
183
184impl From<crate::snowflake::SnowflakeError> for AppError {
185 fn from(err: crate::snowflake::SnowflakeError) -> Self {
186 AppError::Internal(err.to_string())
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_not_found_error() {
202 let err = AppError::NotFound("User not found".to_string());
203 assert_eq!(err.to_string(), "Resource not found: User not found");
204 }
205
206 #[test]
207 fn test_validation_error() {
208 let err = AppError::Validation("Invalid email".to_string());
209 assert_eq!(err.to_string(), "Validation error: Invalid email");
210 }
211}