You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

156 lines
5.0 KiB
Python

# Define base error model
import logging
from typing import Any, Dict, List, Optional
from asyncpg.exceptions import DataError as AsyncPGDataError
from asyncpg.exceptions import PostgresError
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from sqlalchemy.exc import DataError, DBAPIError, IntegrityError, SQLAlchemyError
from src.enums import ResponseStatus
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()
],
},
)