126 lines
4.0 KiB
Rust
126 lines
4.0 KiB
Rust
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<T, ApiError>` is aliased as `ApiResult<T>` 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<sqlx::Error> for ApiError {
|
|
fn from(e: sqlx::Error) -> Self {
|
|
ApiError::Database(e)
|
|
}
|
|
}
|
|
|
|
impl From<std::io::Error> for ApiError {
|
|
fn from(e: std::io::Error) -> Self {
|
|
ApiError::System(e)
|
|
}
|
|
}
|
|
|
|
impl From<axum::extract::multipart::MultipartError> for ApiError {
|
|
fn from(e: axum::extract::multipart::MultipartError) -> Self {
|
|
ApiError::Internal(format!("Multipart error: {}", e))
|
|
}
|
|
}
|
|
|
|
impl From<ParseIntError> for ApiError {
|
|
fn from(e: ParseIntError) -> Self {
|
|
ApiError::BadRequest(format!("Int error: {}", e))
|
|
}
|
|
}
|
|
|
|
impl From<anyhow::Error> for ApiError {
|
|
fn from(e: anyhow::Error) -> Self {
|
|
ApiError::Internal(format!("Internal error: {}", e))
|
|
}
|
|
}
|
|
|
|
impl From<serde_json::Error> 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<String>` 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<T> = result::Result<T, ApiError>;
|
|
|
|
impl ApiError {
|
|
/// Helper to create a generic internal error (also logs).
|
|
pub fn internal<M: Into<String>>(msg: M) -> Self {
|
|
let msg = msg.into();
|
|
log::error!("Internal error created: {}", msg);
|
|
ApiError::Internal(msg)
|
|
}
|
|
}
|