//! API error types and response structures. use axum::Json; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use serde::{Deserialize, Serialize}; use thiserror::Error; /// Unified API result type. pub type ApiResult = std::result::Result; /// API-level errors. #[derive(Debug, Error)] pub enum ApiError { #[error("unauthenticated: {0}")] Unauthenticated(String), #[error("forbidden: {0}")] Forbidden(String), #[error("not found: {0}")] NotFound(String), #[error("conflict: {0}")] Conflict(String), #[error("bad request: {0}")] BadRequest(String), #[error("validation error: {0}")] Validation(String), #[error("rate limited: {0}")] RateLimited(String), #[error("internal server error: {0}")] Internal(String), #[error("subsystem unavailable: {0}")] Unavailable(String), } impl From for ApiError { fn from(err: nx9_wg_db::DbError) -> Self { match err { nx9_wg_db::DbError::NotFound(msg) => Self::NotFound(msg), nx9_wg_db::DbError::Conflict(msg) => Self::Conflict(msg), nx9_wg_db::DbError::ConstraintViolation(msg) => Self::BadRequest(msg), nx9_wg_db::DbError::Validation(msg) => Self::Validation(msg), nx9_wg_db::DbError::Sqlx(e) => { tracing::error!("database error: {e}"); Self::Internal("A database error occurred".to_string()) } nx9_wg_db::DbError::Migration(msg) => Self::Internal(format!("Migration error: {msg}")), nx9_wg_db::DbError::Internal(msg) => Self::Internal(msg), } } } impl From for ApiError { fn from(err: nx9_wg_core::error::Nx9Error) -> Self { match err { nx9_wg_core::error::Nx9Error::Validation(msg) => Self::Validation(msg), nx9_wg_core::error::Nx9Error::Auth(msg) => Self::Unauthenticated(msg), nx9_wg_core::error::Nx9Error::Crypto(msg) => Self::Internal(msg), nx9_wg_core::error::Nx9Error::Config(msg) => Self::BadRequest(msg), _ => Self::Internal(err.to_string()), } } } /// Standard JSON error envelope. #[derive(Debug, Serialize, Deserialize)] pub struct ErrorResponse { pub error: ErrorBody, } /// Error details in the error response. #[derive(Debug, Serialize, Deserialize)] pub struct ErrorBody { pub code: String, pub message: String, } impl IntoResponse for ApiError { fn into_response(self) -> Response { let (status, code, message) = match &self { Self::Unauthenticated(msg) => { (StatusCode::UNAUTHORIZED, "UNAUTHENTICATED", msg.clone()) } Self::Forbidden(msg) => (StatusCode::FORBIDDEN, "FORBIDDEN", msg.clone()), Self::NotFound(msg) => (StatusCode::NOT_FOUND, "NOT_FOUND", msg.clone()), Self::Conflict(msg) => (StatusCode::CONFLICT, "CONFLICT", msg.clone()), Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, "BAD_REQUEST", msg.clone()), Self::Validation(msg) => ( StatusCode::UNPROCESSABLE_ENTITY, "VALIDATION_ERROR", msg.clone(), ), Self::RateLimited(msg) => (StatusCode::TOO_MANY_REQUESTS, "RATE_LIMITED", msg.clone()), Self::Internal(msg) => { tracing::error!("Internal server error: {msg}"); ( StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "An unexpected error occurred".to_string(), ) } Self::Unavailable(msg) => ( StatusCode::SERVICE_UNAVAILABLE, "SUBSYSTEM_UNAVAILABLE", msg.clone(), ), }; let body = Json(ErrorResponse { error: ErrorBody { code: code.to_string(), message, }, }); (status, body).into_response() } }