mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 10:20:32 +00:00
feat: add typed error handling with thiserror
- AppError enum with IntoResponse for Axum - Automatic conversion from StorageError and ValidationError - JSON error responses with request_id support
This commit is contained in:
121
nora-registry/src/error.rs
Normal file
121
nora-registry/src/error.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
//! Application error handling with HTTP response conversion
|
||||||
|
//!
|
||||||
|
//! Provides a unified error type that can be converted to HTTP responses
|
||||||
|
//! with appropriate status codes and JSON error bodies.
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::storage::StorageError;
|
||||||
|
use crate::validation::ValidationError;
|
||||||
|
|
||||||
|
/// Application-level errors with HTTP response conversion
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AppError {
|
||||||
|
#[error("Not found: {0}")]
|
||||||
|
NotFound(String),
|
||||||
|
|
||||||
|
#[error("Bad request: {0}")]
|
||||||
|
BadRequest(String),
|
||||||
|
|
||||||
|
#[error("Unauthorized: {0}")]
|
||||||
|
Unauthorized(String),
|
||||||
|
|
||||||
|
#[error("Internal error: {0}")]
|
||||||
|
Internal(String),
|
||||||
|
|
||||||
|
#[error("Storage error: {0}")]
|
||||||
|
Storage(#[from] StorageError),
|
||||||
|
|
||||||
|
#[error("Validation error: {0}")]
|
||||||
|
Validation(#[from] ValidationError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JSON error response body
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ErrorResponse {
|
||||||
|
error: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
request_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let (status, message) = match &self {
|
||||||
|
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
|
||||||
|
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
|
||||||
|
AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
|
||||||
|
AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()),
|
||||||
|
AppError::Storage(e) => match e {
|
||||||
|
StorageError::NotFound => (StatusCode::NOT_FOUND, "Resource not found".to_string()),
|
||||||
|
StorageError::Validation(v) => (StatusCode::BAD_REQUEST, v.to_string()),
|
||||||
|
_ => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
|
||||||
|
},
|
||||||
|
AppError::Validation(e) => (StatusCode::BAD_REQUEST, e.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
status,
|
||||||
|
Json(ErrorResponse {
|
||||||
|
error: message,
|
||||||
|
request_id: None,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppError {
|
||||||
|
/// Create a not found error
|
||||||
|
pub fn not_found(msg: impl Into<String>) -> Self {
|
||||||
|
Self::NotFound(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a bad request error
|
||||||
|
pub fn bad_request(msg: impl Into<String>) -> Self {
|
||||||
|
Self::BadRequest(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an unauthorized error
|
||||||
|
pub fn unauthorized(msg: impl Into<String>) -> Self {
|
||||||
|
Self::Unauthorized(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an internal error
|
||||||
|
pub fn internal(msg: impl Into<String>) -> Self {
|
||||||
|
Self::Internal(msg.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_app_error_from_storage_error() {
|
||||||
|
let storage_err = StorageError::NotFound;
|
||||||
|
let app_err: AppError = storage_err.into();
|
||||||
|
assert!(matches!(app_err, AppError::Storage(StorageError::NotFound)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_app_error_from_validation_error() {
|
||||||
|
let val_err = ValidationError::EmptyInput;
|
||||||
|
let app_err: AppError = val_err.into();
|
||||||
|
assert!(matches!(
|
||||||
|
app_err,
|
||||||
|
AppError::Validation(ValidationError::EmptyInput)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_display() {
|
||||||
|
let err = AppError::NotFound("image not found".to_string());
|
||||||
|
assert_eq!(err.to_string(), "Not found: image not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user