# Define base error model import logging from typing import Any, Dict, List, Optional from fastapi import FastAPI, HTTPException, Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from pydantic import BaseModel from src.enums import ResponseStatus from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from sqlalchemy.exc import SQLAlchemyError, IntegrityError, DataError, DBAPIError from asyncpg.exceptions import DataError as AsyncPGDataError, PostgresError class ErrorDetail(BaseModel): field: Optional[str] = None message: str code: Optional[str] = None params: Optional[Dict[str, Any]] = None class ErrorResponse(BaseModel): data: Optional[Any] = None message: str status: ResponseStatus = ResponseStatus.ERROR errors: Optional[List[ErrorDetail]] = None # Custom exception handler setup def get_request_context(request: Request): """ Get detailed request context for logging. """ def get_client_ip(): """ Get the real client IP address from Kong Gateway headers. Kong sets X-Real-IP and X-Forwarded-For headers by default. """ # Kong specific headers if "X-Real-IP" in request.headers: return request.headers["X-Real-IP"] # Fallback to X-Forwarded-For if "X-Forwarded-For" in request.headers: # Get the first IP (original client) return request.headers["X-Forwarded-For"].split(",")[0].strip() # Last resort return request.client.host return { "endpoint": request.url.path, "url": request.url, "method": request.method, "remote_addr": get_client_ip(), } def handle_sqlalchemy_error(error: SQLAlchemyError): """ Handle SQLAlchemy errors and return user-friendly error messages. """ original_error = getattr(error, 'orig', None) print(original_error) if isinstance(error, IntegrityError): if "unique constraint" in str(error).lower(): return "This record already exists.", 409 elif "foreign key constraint" in str(error).lower(): return "Related record not found.", 400 else: return "Data integrity error.", 400 elif isinstance(error, DataError) or isinstance(original_error, AsyncPGDataError): return "Invalid data provided.", 400 elif isinstance(error, DBAPIError): if "unique constraint" in str(error).lower(): return "This record already exists.", 409 elif "foreign key constraint" in str(error).lower(): return "Related record not found.", 400 elif "null value in column" in str(error).lower(): return "Required data missing.", 400 elif "invalid input for query argument" in str(error).lower(): return "Invalid data provided.", 400 else: return "Database error.", 500 else: # Log the full error for debugging purposes logging.error(f"Unexpected database error: {str(error)}") return "An unexpected database error occurred.", 500 def handle_exception(request: Request, exc: Exception): """ Global exception handler for Fastapi application. """ request_info = get_request_context(request) if isinstance(exc, RateLimitExceeded): _rate_limit_exceeded_handler(request, exc) if isinstance(exc, HTTPException): logging.error( f"HTTP exception | Code: {exc.status_code} | Error: {exc.detail} | Request: {request_info}", extra={"error_category": "http"}, ) return JSONResponse( status_code=exc.status_code, content={ "data": None, "message": str(exc.detail), "status": ResponseStatus.ERROR, "errors": [ ErrorDetail( message=str(exc.detail) ).model_dump() ] } ) if isinstance(exc, SQLAlchemyError): error_message, status_code = handle_sqlalchemy_error(exc) logging.error( f"Database Error | Error: {str(error_message)} | Request: {request_info}", extra={"error_category": "database"}, ) return JSONResponse( status_code=status_code, content={ "data": None, "message": error_message, "status": ResponseStatus.ERROR, "errors": [ ErrorDetail( message=error_message ).model_dump() ] } ) # Log unexpected errors logging.error( f"Unexpected Error | Error: {str(exc)} | Request: {request_info}", extra={"error_category": "unexpected"}, ) return JSONResponse( status_code=500, content={ "data": None, "message": str(exc), "status": ResponseStatus.ERROR, "errors": [ ErrorDetail( message="An unexpected error occurred." ).model_dump() ] } )