church-api/src/error.rs
Benjamin Slingo ad43a19f7c Phase 1: Enhance error handling and database operations
- 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
2025-08-28 21:20:29 -04:00

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>;