htknow/src/api/error.rs
Defeng fe8f0a875a
Some checks failed
CI / Frontend (Vite) (push) Successful in 24s
CI / Docker Release Tar (push) Has been skipped
CI / Backend (Rust) (push) Failing after 10m15s
Initial project import
2026-07-24 10:28:31 +08:00

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)
}
}