""" 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)