use axum::{ http::StatusCode, response::{IntoResponse, Response}, Json, }; use thiserror::Error; /// Production-grade error codes for API clients /// Follows Google API error code conventions #[derive(Error, Debug)] pub enum AppError { #[error("Database error: {0}")] Database(#[from] sqlx::Error), #[error("Authentication failed: {0}")] Auth(String), #[error("Authorization failed: {0}")] Forbidden(String), #[error("Invalid request: {0}")] BadRequest(String), #[error("Resource not found: {0}")] NotFound(String), #[error("Internal server error: {0}")] Internal(String), #[error("Validation error: {0}")] Validation(String), #[error("Too many requests")] RateLimited, #[error("Request timeout")] Timeout, #[error("Conflict: {0}")] Conflict(String), #[error("Unprocessable entity: {0}")] UnprocessableEntity(String), } #[derive(serde::Serialize)] pub struct ErrorResponse { pub success: bool, pub error: ErrorDetails, } #[derive(serde::Serialize)] pub struct ErrorDetails { pub code: String, pub message: String, #[serde(skip_serializing_if = "Option::is_none")] pub details: Option, } 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 = Result;