Spaces:
Running
Running
| use axum::{ | |
| http::StatusCode, | |
| response::{IntoResponse, Response}, | |
| Json, | |
| }; | |
| use thiserror::Error; | |
| /// Production-grade error codes for API clients | |
| /// Follows Google API error code conventions | |
| pub enum AppError { | |
| Database( sqlx::Error), | |
| Auth(String), | |
| Forbidden(String), | |
| BadRequest(String), | |
| NotFound(String), | |
| Internal(String), | |
| Validation(String), | |
| RateLimited, | |
| Timeout, | |
| Conflict(String), | |
| UnprocessableEntity(String), | |
| } | |
| pub struct ErrorResponse { | |
| pub success: bool, | |
| pub error: ErrorDetails, | |
| } | |
| pub struct ErrorDetails { | |
| pub code: String, | |
| pub message: String, | |
| pub details: Option<String>, | |
| } | |
| impl IntoResponse for AppError { | |
| fn into_response(self) -> Response { | |
| let (status, error_code, message, details) = match self { | |
| AppError::Database(ref e) => { | |
| tracing::error!("Database error: {:?}", e); | |
| ( | |
| StatusCode::INTERNAL_SERVER_ERROR, | |
| "INTERNAL_ERROR", | |
| "Database operation failed".to_string(), | |
| Some(format!("{:?}", e)), | |
| ) | |
| } | |
| AppError::Auth(msg) => { | |
| tracing::warn!("Auth failure: {}", msg); | |
| (StatusCode::UNAUTHORIZED, "UNAUTHENTICATED", msg, None) | |
| } | |
| AppError::Forbidden(msg) => { | |
| tracing::warn!("Authorization failure: {}", msg); | |
| (StatusCode::FORBIDDEN, "PERMISSION_DENIED", msg, None) | |
| } | |
| AppError::BadRequest(msg) => { | |
| tracing::debug!("Bad request: {}", msg); | |
| (StatusCode::BAD_REQUEST, "INVALID_ARGUMENT", msg, None) | |
| } | |
| AppError::NotFound(msg) => { | |
| tracing::debug!("Resource not found: {}", msg); | |
| (StatusCode::NOT_FOUND, "NOT_FOUND", msg, None) | |
| } | |
| AppError::Internal(msg) => { | |
| tracing::error!("Internal error: {}", msg); | |
| ( | |
| StatusCode::INTERNAL_SERVER_ERROR, | |
| "INTERNAL_ERROR", | |
| "An unexpected error occurred".to_string(), | |
| Some(msg), | |
| ) | |
| } | |
| AppError::Validation(msg) => { | |
| tracing::debug!("Validation error: {}", msg); | |
| (StatusCode::BAD_REQUEST, "INVALID_ARGUMENT", msg, None) | |
| } | |
| AppError::RateLimited => { | |
| tracing::warn!("Rate limit exceeded"); | |
| ( | |
| StatusCode::TOO_MANY_REQUESTS, | |
| "RESOURCE_EXHAUSTED", | |
| "Too many requests. Please try again later.".to_string(), | |
| None, | |
| ) | |
| } | |
| AppError::Timeout => { | |
| tracing::warn!("Request timeout"); | |
| ( | |
| StatusCode::REQUEST_TIMEOUT, | |
| "DEADLINE_EXCEEDED", | |
| "Request processing timeout. Please try again.".to_string(), | |
| None, | |
| ) | |
| } | |
| AppError::Conflict(msg) => { | |
| tracing::warn!("Conflict: {}", msg); | |
| (StatusCode::CONFLICT, "ALREADY_EXISTS", msg, None) | |
| } | |
| AppError::UnprocessableEntity(msg) => { | |
| tracing::warn!("Unprocessable entity: {}", msg); | |
| ( | |
| StatusCode::UNPROCESSABLE_ENTITY, | |
| "FAILED_PRECONDITION", | |
| msg, | |
| None, | |
| ) | |
| } | |
| }; | |
| let body = Json(ErrorResponse { | |
| success: false, | |
| error: ErrorDetails { | |
| code: error_code.to_string(), | |
| message, | |
| details: if cfg!(debug_assertions) { | |
| details | |
| } else { | |
| None | |
| }, | |
| }, | |
| }); | |
| (status, body).into_response() | |
| } | |
| } | |
| pub type AppResult<T> = Result<T, AppError>; | |