346
web/backend/core/exceptions.py
Normal file
346
web/backend/core/exceptions.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
Custom Exception Classes
|
||||
|
||||
Provides specific exception types for better error handling and reporting.
|
||||
Replaces broad 'except Exception' handlers with specific, meaningful exceptions.
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from fastapi import HTTPException
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# BASE EXCEPTIONS
|
||||
# ============================================================================
|
||||
|
||||
class MediaDownloaderError(Exception):
|
||||
"""Base exception for all Media Downloader errors"""
|
||||
|
||||
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
||||
self.message = message
|
||||
self.details = details or {}
|
||||
super().__init__(self.message)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"error": self.__class__.__name__,
|
||||
"message": self.message,
|
||||
"details": self.details
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DATABASE EXCEPTIONS
|
||||
# ============================================================================
|
||||
|
||||
class DatabaseError(MediaDownloaderError):
|
||||
"""Database operation failed"""
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseConnectionError(DatabaseError):
|
||||
"""Failed to connect to database"""
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseQueryError(DatabaseError):
|
||||
"""Database query failed"""
|
||||
pass
|
||||
|
||||
|
||||
class RecordNotFoundError(DatabaseError):
|
||||
"""Requested record not found in database"""
|
||||
pass
|
||||
|
||||
|
||||
# Alias for generic "not found" scenarios
|
||||
NotFoundError = RecordNotFoundError
|
||||
|
||||
|
||||
class DuplicateRecordError(DatabaseError):
|
||||
"""Attempted to insert duplicate record"""
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DOWNLOAD EXCEPTIONS
|
||||
# ============================================================================
|
||||
|
||||
class DownloadError(MediaDownloaderError):
|
||||
"""Download operation failed"""
|
||||
pass
|
||||
|
||||
|
||||
class NetworkError(DownloadError):
|
||||
"""Network request failed"""
|
||||
pass
|
||||
|
||||
|
||||
class RateLimitError(DownloadError):
|
||||
"""Rate limit exceeded"""
|
||||
pass
|
||||
|
||||
|
||||
class AuthenticationError(DownloadError):
|
||||
"""Authentication failed for external service"""
|
||||
pass
|
||||
|
||||
|
||||
class PlatformUnavailableError(DownloadError):
|
||||
"""Platform or service is unavailable"""
|
||||
pass
|
||||
|
||||
|
||||
class ContentNotFoundError(DownloadError):
|
||||
"""Requested content not found on platform"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidURLError(DownloadError):
|
||||
"""Invalid or malformed URL"""
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# FILE OPERATION EXCEPTIONS
|
||||
# ============================================================================
|
||||
|
||||
class FileOperationError(MediaDownloaderError):
|
||||
"""File operation failed"""
|
||||
pass
|
||||
|
||||
|
||||
class MediaFileNotFoundError(FileOperationError):
|
||||
"""File not found on filesystem"""
|
||||
pass
|
||||
|
||||
|
||||
class FileAccessError(FileOperationError):
|
||||
"""Cannot access file (permissions, etc.)"""
|
||||
pass
|
||||
|
||||
|
||||
class FileHashError(FileOperationError):
|
||||
"""File hash computation failed"""
|
||||
pass
|
||||
|
||||
|
||||
class FileMoveError(FileOperationError):
|
||||
"""Failed to move file"""
|
||||
pass
|
||||
|
||||
|
||||
class ThumbnailError(FileOperationError):
|
||||
"""Thumbnail generation failed"""
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION EXCEPTIONS
|
||||
# ============================================================================
|
||||
|
||||
class ValidationError(MediaDownloaderError):
|
||||
"""Input validation failed"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidParameterError(ValidationError):
|
||||
"""Invalid parameter value"""
|
||||
pass
|
||||
|
||||
|
||||
class MissingParameterError(ValidationError):
|
||||
"""Required parameter missing"""
|
||||
pass
|
||||
|
||||
|
||||
class PathTraversalError(ValidationError):
|
||||
"""Path traversal attempt detected"""
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# AUTHENTICATION/AUTHORIZATION EXCEPTIONS
|
||||
# ============================================================================
|
||||
|
||||
class AuthError(MediaDownloaderError):
|
||||
"""Authentication or authorization error"""
|
||||
pass
|
||||
|
||||
|
||||
class TokenExpiredError(AuthError):
|
||||
"""Authentication token has expired"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidTokenError(AuthError):
|
||||
"""Authentication token is invalid"""
|
||||
pass
|
||||
|
||||
|
||||
class InsufficientPermissionsError(AuthError):
|
||||
"""User lacks required permissions"""
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SERVICE EXCEPTIONS
|
||||
# ============================================================================
|
||||
|
||||
class ServiceError(MediaDownloaderError):
|
||||
"""External service error"""
|
||||
pass
|
||||
|
||||
|
||||
class FlareSolverrError(ServiceError):
|
||||
"""FlareSolverr service error"""
|
||||
pass
|
||||
|
||||
|
||||
class RedisError(ServiceError):
|
||||
"""Redis cache error"""
|
||||
pass
|
||||
|
||||
|
||||
class SchedulerError(ServiceError):
|
||||
"""Scheduler service error"""
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# FACE RECOGNITION EXCEPTIONS
|
||||
# ============================================================================
|
||||
|
||||
class FaceRecognitionError(MediaDownloaderError):
|
||||
"""Face recognition operation failed"""
|
||||
pass
|
||||
|
||||
|
||||
class NoFaceDetectedError(FaceRecognitionError):
|
||||
"""No face detected in image"""
|
||||
pass
|
||||
|
||||
|
||||
class FaceEncodingError(FaceRecognitionError):
|
||||
"""Failed to encode face"""
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HTTP EXCEPTION HELPERS
|
||||
# ============================================================================
|
||||
|
||||
def to_http_exception(error: MediaDownloaderError) -> HTTPException:
|
||||
"""Convert a MediaDownloaderError to an HTTPException"""
|
||||
|
||||
# Map exception types to HTTP status codes (most-specific subclasses first)
|
||||
status_map = {
|
||||
# 400 Bad Request (subclasses before parent)
|
||||
InvalidParameterError: 400,
|
||||
MissingParameterError: 400,
|
||||
InvalidURLError: 400,
|
||||
ValidationError: 400,
|
||||
|
||||
# 401 Unauthorized (subclasses before parent)
|
||||
TokenExpiredError: 401,
|
||||
InvalidTokenError: 401,
|
||||
AuthenticationError: 401,
|
||||
AuthError: 401,
|
||||
|
||||
# 403 Forbidden
|
||||
InsufficientPermissionsError: 403,
|
||||
PathTraversalError: 403,
|
||||
FileAccessError: 403,
|
||||
|
||||
# 404 Not Found
|
||||
RecordNotFoundError: 404,
|
||||
MediaFileNotFoundError: 404,
|
||||
ContentNotFoundError: 404,
|
||||
|
||||
# 409 Conflict
|
||||
DuplicateRecordError: 409,
|
||||
|
||||
# 429 Too Many Requests
|
||||
RateLimitError: 429,
|
||||
|
||||
# 500 Internal Server Error (subclasses before parent)
|
||||
DatabaseConnectionError: 500,
|
||||
DatabaseQueryError: 500,
|
||||
DatabaseError: 500,
|
||||
|
||||
# 502 Bad Gateway (subclasses before parent)
|
||||
FlareSolverrError: 502,
|
||||
ServiceError: 502,
|
||||
NetworkError: 502,
|
||||
|
||||
# 503 Service Unavailable
|
||||
PlatformUnavailableError: 503,
|
||||
SchedulerError: 503,
|
||||
}
|
||||
|
||||
# Find the most specific matching status code
|
||||
status_code = 500 # Default to internal server error
|
||||
for exc_type, code in status_map.items():
|
||||
if isinstance(error, exc_type):
|
||||
status_code = code
|
||||
break
|
||||
|
||||
return HTTPException(
|
||||
status_code=status_code,
|
||||
detail=error.to_dict()
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# EXCEPTION HANDLER DECORATOR
|
||||
# ============================================================================
|
||||
|
||||
from functools import wraps
|
||||
from typing import Callable, TypeVar
|
||||
import asyncio
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def handle_exceptions(func: Callable[..., T]) -> Callable[..., T]:
|
||||
"""
|
||||
Decorator to convert MediaDownloaderError exceptions to HTTPException.
|
||||
Use this on route handlers for consistent error responses.
|
||||
"""
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except MediaDownloaderError as e:
|
||||
raise to_http_exception(e)
|
||||
except HTTPException:
|
||||
raise # Let HTTPException pass through
|
||||
except Exception as e:
|
||||
# Log unexpected exceptions
|
||||
from modules.universal_logger import get_logger
|
||||
logger = get_logger('API')
|
||||
logger.error(f"Unexpected error in {func.__name__}: {e}", module="Core")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": "InternalError", "message": str(e)}
|
||||
)
|
||||
return async_wrapper
|
||||
else:
|
||||
@wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except MediaDownloaderError as e:
|
||||
raise to_http_exception(e)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
from modules.universal_logger import get_logger
|
||||
logger = get_logger('API')
|
||||
logger.error(f"Unexpected error in {func.__name__}: {e}", module="Core")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": "InternalError", "message": str(e)}
|
||||
)
|
||||
return sync_wrapper
|
||||
Reference in New Issue
Block a user