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 from app.utils.logger import get_logger # 로거 설정 logger = get_logger("core") 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.debug(traceback.format_exc()) 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.debug(traceback.format_exc()) 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.debug(traceback.format_exc()) 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.debug(traceback.format_exc()) raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="데이터베이스 연결에 문제가 발생했습니다.", ) except Exception as e: logger.error(f"[API Error] {func.__name__}: {e}") logger.debug(traceback.format_exc()) 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: logger.debug(f"Handled Exception: {exception.__class__.__name__}") # 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__, ), ) # SocialException 핸들러 추가 from app.social.exceptions import SocialException @app.exception_handler(SocialException) def social_exception_handler(request: Request, exc: SocialException) -> Response: logger.debug(f"Handled SocialException: {exc.__class__.__name__} - {exc.message}") return JSONResponse( status_code=exc.status_code, content={ "detail": exc.message, "code": exc.code, }, ) @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}", } )