import logging import traceback from functools import wraps from typing import Any, Callable, TypeVar from fastapi import FastAPI, HTTPException, Request, Response, status from fastapi.responses import JSONResponse from sqlalchemy.exc import SQLAlchemyError # 로거 설정 logger = logging.getLogger(__name__) T = TypeVar("T") class FastShipError(Exception): """Base exception for all exceptions in fastship api""" # status_code to be returned for this exception # when it is handled status = status.HTTP_400_BAD_REQUEST class EntityNotFound(FastShipError): """Entity not found in database""" status = status.HTTP_404_NOT_FOUND class BadPassword(FastShipError): """Password is not strong enough or invalid""" status = status.HTTP_400_BAD_REQUEST class ClientNotAuthorized(FastShipError): """Client is not authorized to perform the action""" status = status.HTTP_401_UNAUTHORIZED class ClientNotVerified(FastShipError): """Client is not verified""" status = status.HTTP_401_UNAUTHORIZED class NothingToUpdate(FastShipError): """No data provided to update""" class BadCredentials(FastShipError): """User email or password is incorrect""" status = status.HTTP_401_UNAUTHORIZED class InvalidToken(FastShipError): """Access token is invalid or expired""" status = status.HTTP_401_UNAUTHORIZED class DeliveryPartnerNotAvailable(FastShipError): """Delivery partner/s do not service the destination""" status = status.HTTP_406_NOT_ACCEPTABLE class DeliveryPartnerCapacityExceeded(FastShipError): """Delivery partner has reached their max handling capacity""" status = status.HTTP_406_NOT_ACCEPTABLE # ============================================================================= # 데이터베이스 관련 예외 # ============================================================================= class DatabaseError(FastShipError): """Database operation failed""" status = status.HTTP_503_SERVICE_UNAVAILABLE class DatabaseConnectionError(DatabaseError): """Database connection failed""" status = status.HTTP_503_SERVICE_UNAVAILABLE class DatabaseTimeoutError(DatabaseError): """Database operation timed out""" status = status.HTTP_504_GATEWAY_TIMEOUT # ============================================================================= # 외부 서비스 관련 예외 # ============================================================================= class ExternalServiceError(FastShipError): """External service call failed""" status = status.HTTP_502_BAD_GATEWAY class GPTServiceError(ExternalServiceError): """GPT API call failed""" status = status.HTTP_502_BAD_GATEWAY class CrawlingError(ExternalServiceError): """Web crawling failed""" status = status.HTTP_502_BAD_GATEWAY class BlobStorageError(ExternalServiceError): """Azure Blob Storage operation failed""" status = status.HTTP_502_BAD_GATEWAY class CreatomateError(ExternalServiceError): """Creatomate API call failed""" status = status.HTTP_502_BAD_GATEWAY # ============================================================================= # 예외 처리 데코레이터 # ============================================================================= def handle_db_exceptions( error_message: str = "데이터베이스 작업 중 오류가 발생했습니다.", ): """데이터베이스 예외를 처리하는 데코레이터. Args: error_message: 오류 발생 시 반환할 메시지 Example: @handle_db_exceptions("사용자 조회 중 오류 발생") async def get_user(user_id: int): ... """ def decorator(func: Callable[..., Any]) -> Callable[..., Any]: @wraps(func) async def wrapper(*args: Any, **kwargs: Any) -> Any: try: return await func(*args, **kwargs) except HTTPException: # HTTPException은 그대로 raise raise except SQLAlchemyError as e: logger.error(f"[DB Error] {func.__name__}: {e}") logger.error(traceback.format_exc()) print(f"[DB Error] {func.__name__}: {e}") raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=error_message, ) except Exception as e: logger.error(f"[Unexpected Error] {func.__name__}: {e}") logger.error(traceback.format_exc()) print(f"[Unexpected Error] {func.__name__}: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="서비스 처리 중 예기치 않은 오류가 발생했습니다.", ) return wrapper return decorator def handle_external_service_exceptions( service_name: str = "외부 서비스", error_message: str | None = None, ): """외부 서비스 호출 예외를 처리하는 데코레이터. Args: service_name: 서비스 이름 (로그용) error_message: 오류 발생 시 반환할 메시지 Example: @handle_external_service_exceptions("GPT") async def call_gpt(): ... """ def decorator(func: Callable[..., Any]) -> Callable[..., Any]: @wraps(func) async def wrapper(*args: Any, **kwargs: Any) -> Any: try: return await func(*args, **kwargs) except HTTPException: raise except Exception as e: msg = error_message or f"{service_name} 서비스 호출 중 오류가 발생했습니다." logger.error(f"[{service_name} Error] {func.__name__}: {e}") logger.error(traceback.format_exc()) print(f"[{service_name} Error] {func.__name__}: {e}") raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=msg, ) return wrapper return decorator def handle_api_exceptions( error_message: str = "요청 처리 중 오류가 발생했습니다.", ): """API 엔드포인트 예외를 처리하는 데코레이터. Args: error_message: 오류 발생 시 반환할 메시지 Example: @handle_api_exceptions("가사 생성 중 오류 발생") async def generate_lyric(): ... """ def decorator(func: Callable[..., Any]) -> Callable[..., Any]: @wraps(func) async def wrapper(*args: Any, **kwargs: Any) -> Any: try: return await func(*args, **kwargs) except HTTPException: raise except SQLAlchemyError as e: logger.error(f"[API DB Error] {func.__name__}: {e}") logger.error(traceback.format_exc()) print(f"[API DB Error] {func.__name__}: {e}") raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="데이터베이스 연결에 문제가 발생했습니다.", ) except Exception as e: logger.error(f"[API Error] {func.__name__}: {e}") logger.error(traceback.format_exc()) print(f"[API Error] {func.__name__}: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=error_message, ) return wrapper return decorator def _get_handler(status: int, detail: str): # Define def handler(request: Request, exception: Exception) -> Response: # DEBUG PRINT STATEMENT 👇 from rich import print, panel print( panel.Panel( exception.__class__.__name__, title="Handled Exception", border_style="red", ), ) # DEBUG PRINT STATEMENT 👆 # Raise HTTPException with given status and detail # can return JSONResponse as well raise HTTPException( status_code=status, detail=detail, ) # Return ExceptionHandler required with given # status and detail for HTTPExcetion above return handler def add_exception_handlers(app: FastAPI): # Get all subclass of 👇, our custom exceptions exception_classes = FastShipError.__subclasses__() for exception_class in exception_classes: # Add exception handler app.add_exception_handler( # Custom exception class exception_class, # Get handler function _get_handler( status=exception_class.status, detail=exception_class.__doc__, ), ) @app.exception_handler(status.HTTP_500_INTERNAL_SERVER_ERROR) def internal_server_error_handler(request, exception): return JSONResponse( content={"detail": "Something went wrong..."}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, headers={ "X-Error": f"{exception}", } )