use std::{fmt, num::ParseIntError, result}; use axum::{ Json, http::StatusCode, response::{IntoResponse, Response} }; use serde::Serialize; use sqlx; /// Unified API error type for handlers to return. /// /// Example: /// - `Result` is aliased as `ApiResult` below. /// - Convert database errors with `ApiError::from(sqlx::Error)`. #[derive(Debug)] pub enum ApiError { BadRequest(String), // Unauthorized(String), Forbidden(String), NotFound(String), Database(sqlx::Error), System(std::io::Error), Internal(String), } impl fmt::Display for ApiError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ApiError::BadRequest(msg) => write!(f, "BadRequest: {}", msg), // ApiError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg), ApiError::Forbidden(msg) => write!(f, "Forbidden: {}", msg), ApiError::NotFound(msg) => write!(f, "NotFound: {}", msg), ApiError::Database(e) => write!(f, "Database error: {}", e), ApiError::System(e) => write!(f, "System error: {}", e), ApiError::Internal(msg) => write!(f, "Internal error: {}", msg), } } } impl std::error::Error for ApiError {} impl From for ApiError { fn from(e: sqlx::Error) -> Self { ApiError::Database(e) } } impl From for ApiError { fn from(e: std::io::Error) -> Self { ApiError::System(e) } } impl From for ApiError { fn from(e: axum::extract::multipart::MultipartError) -> Self { ApiError::Internal(format!("Multipart error: {}", e)) } } impl From for ApiError { fn from(e: ParseIntError) -> Self { ApiError::BadRequest(format!("Int error: {}", e)) } } impl From for ApiError { fn from(e: anyhow::Error) -> Self { ApiError::Internal(format!("Internal error: {}", e)) } } impl From for ApiError { fn from(e: serde_json::Error) -> Self { ApiError::BadRequest(format!("JSON error: {}", e)) } } /// Standardized JSON body returned for errors. #[derive(Serialize)] struct ErrorBody { code: u16, error: String, // optional: you can add `details: Option` if you want more info } impl IntoResponse for ApiError { fn into_response(self) -> Response { // Map each error variant to an HTTP status code and message. let (status, message) = match &self { ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()), // ApiError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()), ApiError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg.clone()), ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), ApiError::Database(e) => { // Log detailed error server-side but avoid leaking internals to clients. log::error!("Database error: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".into()) } ApiError::System(e) => { // Log detailed error server-side but avoid leaking internals to clients. log::error!("System error: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".into()) } ApiError::Internal(msg) => { log::error!("Internal error: {}", msg); (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()) } }; let body = ErrorBody { code: status.as_u16(), error: message }; (status, Json(body)).into_response() } } /// Convenience alias for handlers' return type. pub type ApiResult = result::Result; impl ApiError { /// Helper to create a generic internal error (also logs). pub fn internal>(msg: M) -> Self { let msg = msg.into(); log::error!("Internal error created: {}", msg); ApiError::Internal(msg) } }