feat(api): implement centralized exception handling for unified error responses

- Add custom_exception_handler to handle exceptions in a unified format
- Log errors with request context and provide debug information in development mode
- Update settings.py to use the new exception handler
- Export custom_exception_handler in __init__.py for accessibility

This enhances error management across the API, improving debugging and user experience.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-14 15:50:26 +00:00
parent b6cd544791
commit 66b1868672
3 changed files with 216 additions and 1 deletions

View File

@@ -3,6 +3,7 @@ IGNY8 API Module
"""
from .response import success_response, error_response, paginated_response
from .exception_handlers import custom_exception_handler
__all__ = ['success_response', 'error_response', 'paginated_response']
__all__ = ['success_response', 'error_response', 'paginated_response', 'custom_exception_handler']

View File

@@ -0,0 +1,213 @@
"""
Centralized Exception Handler for IGNY8 API
This module provides a comprehensive exception handler that:
- Wraps all exceptions in unified error format
- Logs errors for debugging and monitoring
- Provides debug information in development mode
- Tracks request IDs for error correlation
"""
import logging
import traceback
import uuid
from django.conf import settings
from rest_framework.views import exception_handler as drf_exception_handler
from rest_framework import status
from rest_framework.response import Response
from igny8_core.api.response import error_response
logger = logging.getLogger(__name__)
def get_request_id(request):
"""
Get or create request ID for error tracking.
Request ID can be set by middleware or generated here.
"""
# Check if request ID is already set (e.g., by middleware)
request_id = getattr(request, 'request_id', None)
if not request_id:
# Generate new request ID
request_id = str(uuid.uuid4())
request.request_id = request_id
return request_id
def extract_error_message(exc, response):
"""
Extract user-friendly error message from exception.
Args:
exc: The exception instance
response: DRF's exception handler response
Returns:
tuple: (error_message, errors_dict)
"""
error_message = "An error occurred"
errors = None
# Handle DRF exceptions with 'detail' attribute
if hasattr(exc, 'detail'):
if isinstance(exc.detail, dict):
# Validation errors - use as errors dict
errors = exc.detail
# Extract first error message as top-level error
if errors:
first_key = list(errors.keys())[0]
first_error = errors[first_key]
if isinstance(first_error, list) and first_error:
error_message = f"{first_key}: {first_error[0]}"
else:
error_message = f"{first_key}: {first_error}"
else:
error_message = "Validation failed"
elif isinstance(exc.detail, list):
# List of errors
error_message = exc.detail[0] if exc.detail else "An error occurred"
errors = {"non_field_errors": exc.detail}
else:
# String error message
error_message = str(exc.detail)
elif response and hasattr(response, 'data'):
# Try to extract from response data
if isinstance(response.data, dict):
# Check for common error message fields
error_message = (
response.data.get('error') or
response.data.get('message') or
response.data.get('detail') or
str(response.data)
)
errors = response.data if 'error' not in response.data else None
elif isinstance(response.data, list):
error_message = response.data[0] if response.data else "An error occurred"
errors = {"non_field_errors": response.data}
else:
error_message = str(response.data) if response.data else "An error occurred"
else:
# Generic exception
error_message = str(exc) if exc else "An error occurred"
return error_message, errors
def get_status_code_message(status_code):
"""
Get default error message for HTTP status code.
"""
status_messages = {
status.HTTP_400_BAD_REQUEST: "Bad request",
status.HTTP_401_UNAUTHORIZED: "Authentication required",
status.HTTP_403_FORBIDDEN: "Permission denied",
status.HTTP_404_NOT_FOUND: "Resource not found",
status.HTTP_405_METHOD_NOT_ALLOWED: "Method not allowed",
status.HTTP_409_CONFLICT: "Conflict",
status.HTTP_422_UNPROCESSABLE_ENTITY: "Unprocessable entity",
status.HTTP_429_TOO_MANY_REQUESTS: "Too many requests",
status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal server error",
status.HTTP_502_BAD_GATEWAY: "Bad gateway",
status.HTTP_503_SERVICE_UNAVAILABLE: "Service unavailable",
}
return status_messages.get(status_code, "An error occurred")
def custom_exception_handler(exc, context):
"""
Centralized exception handler that wraps all exceptions in unified format.
This handler:
- Wraps all exceptions in unified error format
- Logs errors with request context
- Provides debug information in development mode
- Tracks request IDs for error correlation
Args:
exc: The exception instance
context: Dictionary containing request, view, and args
Returns:
Response: Error response in unified format, or None to use default
"""
# Get request from context
request = context.get('request')
view = context.get('view')
# Get request ID for tracking
request_id = None
if request:
request_id = get_request_id(request)
# Call DRF's default exception handler first
response = drf_exception_handler(exc, context)
# Determine status code
if response is not None:
status_code = response.status_code
else:
# Unhandled exception - default to 500
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
# Extract error message and details
error_message, errors = extract_error_message(exc, response)
# Use default message if error message is generic
if error_message == "An error occurred" or not error_message:
error_message = get_status_code_message(status_code)
# Log the error
log_level = logging.ERROR if status_code >= 500 else logging.WARNING
log_message = f"API Error [{status_code}]: {error_message}"
if request_id:
log_message += f" (Request ID: {request_id})"
# Include exception details in log
logger.log(
log_level,
log_message,
extra={
'request_id': request_id,
'status_code': status_code,
'exception_type': type(exc).__name__,
'view': view.__class__.__name__ if view else None,
'path': request.path if request else None,
'method': request.method if request else None,
},
exc_info=status_code >= 500, # Include traceback for server errors
)
# Build error response
error_response_data = {
"success": False,
"error": error_message,
}
# Add errors dict if present
if errors:
error_response_data["errors"] = errors
# Add request ID for error tracking
if request_id:
error_response_data["request_id"] = request_id
# Add debug information in development mode
if settings.DEBUG:
error_response_data["debug"] = {
"exception_type": type(exc).__name__,
"exception_message": str(exc),
"view": view.__class__.__name__ if view else None,
"path": request.path if request else None,
"method": request.method if request else None,
}
# Include traceback in debug mode for server errors
if status_code >= 500:
error_response_data["debug"]["traceback"] = traceback.format_exc()
return Response(error_response_data, status=status_code)