
- Add specific error variants for better internal handling - Enhance error messages with context and logging - Improve database operations with better error handling - Use specific errors for media processing, config issues, duplicates - All HTTP responses remain identical - zero breaking changes - Foundation for better maintainability and debugging
232 lines
8.4 KiB
Rust
232 lines
8.4 KiB
Rust
use axum::{http::StatusCode, response::IntoResponse, Json};
|
|
use serde_json::json;
|
|
|
|
#[derive(Debug)]
|
|
pub enum ApiError {
|
|
DatabaseError(sqlx::Error),
|
|
AuthError(String),
|
|
ValidationError(String),
|
|
NotFound(String),
|
|
BadRequest(String),
|
|
Database(String),
|
|
Internal(String),
|
|
FileError(std::io::Error),
|
|
JwtError(jsonwebtoken::errors::Error),
|
|
BcryptError(bcrypt::BcryptError),
|
|
SerdeError(serde_json::Error),
|
|
|
|
// Enhanced specific error types for better internal handling
|
|
// All map to existing HTTP responses - zero breaking changes
|
|
BulletinNotFound(String),
|
|
EventNotFound(String),
|
|
ScheduleNotFound(String),
|
|
MemberNotFound(String),
|
|
HymnNotFound(String),
|
|
UserNotFound(String),
|
|
|
|
// Processing errors
|
|
BulletinProcessingError(String),
|
|
MediaProcessingError(String),
|
|
EmailSendError(String),
|
|
UploadError(String),
|
|
|
|
// Configuration errors
|
|
ConfigurationError(String),
|
|
MissingConfiguration(String),
|
|
|
|
// Business logic errors
|
|
DuplicateEntry(String),
|
|
InvalidDateRange(String),
|
|
InvalidRecurringPattern(String),
|
|
|
|
// External service errors
|
|
OwncastConnectionError(String),
|
|
ExternalServiceError { service: String, message: String },
|
|
}
|
|
|
|
impl IntoResponse for ApiError {
|
|
fn into_response(self) -> axum::response::Response {
|
|
let (status, message) = match self {
|
|
ApiError::DatabaseError(e) => {
|
|
tracing::error!("Database error: {:?}", e);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "Database error".to_string())
|
|
}
|
|
ApiError::AuthError(msg) => (StatusCode::UNAUTHORIZED, msg),
|
|
ApiError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg),
|
|
ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
|
|
ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
|
|
ApiError::Database(msg) => {
|
|
tracing::error!("Database error: {}", msg);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, msg)
|
|
}
|
|
ApiError::Internal(msg) => {
|
|
tracing::error!("Internal error: {}", msg);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, msg)
|
|
}
|
|
ApiError::FileError(e) => {
|
|
tracing::error!("File error: {:?}", e);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "File operation failed".to_string())
|
|
}
|
|
ApiError::JwtError(e) => {
|
|
tracing::error!("JWT error: {:?}", e);
|
|
(StatusCode::UNAUTHORIZED, "Invalid token".to_string())
|
|
}
|
|
ApiError::BcryptError(e) => {
|
|
tracing::error!("Bcrypt error: {:?}", e);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "Password hashing error".to_string())
|
|
}
|
|
ApiError::SerdeError(e) => {
|
|
tracing::error!("Serde error: {:?}", e);
|
|
(StatusCode::BAD_REQUEST, "Invalid JSON".to_string())
|
|
}
|
|
|
|
// Enhanced error types - map to existing HTTP responses for zero breaking changes
|
|
// All *NotFound variants map to 404
|
|
ApiError::BulletinNotFound(msg) | ApiError::EventNotFound(msg) |
|
|
ApiError::ScheduleNotFound(msg) | ApiError::MemberNotFound(msg) |
|
|
ApiError::HymnNotFound(msg) | ApiError::UserNotFound(msg) => {
|
|
tracing::warn!("Resource not found: {}", msg);
|
|
(StatusCode::NOT_FOUND, msg)
|
|
}
|
|
|
|
// Processing errors map to 500
|
|
ApiError::BulletinProcessingError(msg) | ApiError::MediaProcessingError(msg) => {
|
|
tracing::error!("Processing error: {}", msg);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, msg)
|
|
}
|
|
|
|
// Email and upload errors map to 500
|
|
ApiError::EmailSendError(msg) | ApiError::UploadError(msg) => {
|
|
tracing::error!("Service error: {}", msg);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, msg)
|
|
}
|
|
|
|
// Configuration errors map to 500
|
|
ApiError::ConfigurationError(msg) | ApiError::MissingConfiguration(msg) => {
|
|
tracing::error!("Configuration error: {}", msg);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "Server configuration error".to_string())
|
|
}
|
|
|
|
// Business logic errors map to 400
|
|
ApiError::DuplicateEntry(msg) | ApiError::InvalidDateRange(msg) |
|
|
ApiError::InvalidRecurringPattern(msg) => {
|
|
tracing::warn!("Business logic error: {}", msg);
|
|
(StatusCode::BAD_REQUEST, msg)
|
|
}
|
|
|
|
// External service errors map to 500
|
|
ApiError::OwncastConnectionError(msg) => {
|
|
tracing::error!("Owncast connection error: {}", msg);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "External service unavailable".to_string())
|
|
}
|
|
ApiError::ExternalServiceError { service, message } => {
|
|
tracing::error!("External service '{}' error: {}", service, message);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "External service error".to_string())
|
|
}
|
|
};
|
|
|
|
(
|
|
status,
|
|
Json(json!({
|
|
"success": false,
|
|
"error": message
|
|
})),
|
|
)
|
|
.into_response()
|
|
}
|
|
}
|
|
|
|
impl From<sqlx::Error> for ApiError {
|
|
fn from(error: sqlx::Error) -> Self {
|
|
ApiError::DatabaseError(error)
|
|
}
|
|
}
|
|
|
|
impl From<std::io::Error> for ApiError {
|
|
fn from(error: std::io::Error) -> Self {
|
|
ApiError::FileError(error)
|
|
}
|
|
}
|
|
|
|
impl From<jsonwebtoken::errors::Error> for ApiError {
|
|
fn from(error: jsonwebtoken::errors::Error) -> Self {
|
|
ApiError::JwtError(error)
|
|
}
|
|
}
|
|
|
|
impl From<bcrypt::BcryptError> for ApiError {
|
|
fn from(error: bcrypt::BcryptError) -> Self {
|
|
ApiError::BcryptError(error)
|
|
}
|
|
}
|
|
|
|
impl From<serde_json::Error> for ApiError {
|
|
fn from(error: serde_json::Error) -> Self {
|
|
ApiError::SerdeError(error)
|
|
}
|
|
}
|
|
|
|
impl ApiError {
|
|
// Constructor methods for common patterns - makes code more readable and consistent
|
|
pub fn bulletin_not_found(id: impl std::fmt::Display) -> Self {
|
|
Self::BulletinNotFound(format!("Bulletin not found: {}", id))
|
|
}
|
|
|
|
pub fn event_not_found(id: impl std::fmt::Display) -> Self {
|
|
Self::EventNotFound(format!("Event not found: {}", id))
|
|
}
|
|
|
|
pub fn schedule_not_found(date: impl std::fmt::Display) -> Self {
|
|
Self::ScheduleNotFound(format!("Schedule not found for date: {}", date))
|
|
}
|
|
|
|
pub fn hymn_not_found(hymnal: &str, number: i32) -> Self {
|
|
Self::HymnNotFound(format!("Hymn {} not found in {}", number, hymnal))
|
|
}
|
|
|
|
pub fn user_not_found(identifier: impl std::fmt::Display) -> Self {
|
|
Self::UserNotFound(format!("User not found: {}", identifier))
|
|
}
|
|
|
|
pub fn member_not_found(id: impl std::fmt::Display) -> Self {
|
|
Self::MemberNotFound(format!("Member not found: {}", id))
|
|
}
|
|
|
|
pub fn bulletin_processing_failed(reason: impl std::fmt::Display) -> Self {
|
|
Self::BulletinProcessingError(format!("Bulletin processing failed: {}", reason))
|
|
}
|
|
|
|
pub fn media_processing_failed(reason: impl std::fmt::Display) -> Self {
|
|
Self::MediaProcessingError(format!("Media processing failed: {}", reason))
|
|
}
|
|
|
|
pub fn email_send_failed(reason: impl std::fmt::Display) -> Self {
|
|
Self::EmailSendError(format!("Email sending failed: {}", reason))
|
|
}
|
|
|
|
pub fn upload_failed(reason: impl std::fmt::Display) -> Self {
|
|
Self::UploadError(format!("Upload failed: {}", reason))
|
|
}
|
|
|
|
pub fn invalid_date_range(start: impl std::fmt::Display, end: impl std::fmt::Display) -> Self {
|
|
Self::InvalidDateRange(format!("Invalid date range: {} to {}", start, end))
|
|
}
|
|
|
|
pub fn duplicate_entry(resource: &str, identifier: impl std::fmt::Display) -> Self {
|
|
Self::DuplicateEntry(format!("{} already exists: {}", resource, identifier))
|
|
}
|
|
|
|
pub fn missing_config(key: &str) -> Self {
|
|
Self::MissingConfiguration(format!("Missing required configuration: {}", key))
|
|
}
|
|
|
|
pub fn external_service_failed(service: &str, message: impl std::fmt::Display) -> Self {
|
|
Self::ExternalServiceError {
|
|
service: service.to_string(),
|
|
message: message.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type Result<T> = std::result::Result<T, ApiError>;
|