""" Centralized Exception Handler Wraps all exceptions in unified format """ import logging from rest_framework.views import exception_handler from rest_framework import status from django.conf import settings from django.core.exceptions import ValidationError from django.db import IntegrityError from .response import get_request_id, error_response logger = logging.getLogger(__name__) def custom_exception_handler(exc, context): """ Custom exception handler that wraps all errors in unified format Args: exc: The exception that was raised context: Dictionary containing request, view, args, kwargs Returns: Response object with unified error format """ # Get request from context request = context.get('request') # Get request ID request_id = get_request_id(request) if request else None # Call DRF's default exception handler first response = exception_handler(exc, context) # If DRF handled it, wrap it in unified format if response is not None: # Extract error details from DRF response error_message = None errors = None status_code = response.status_code # Try to extract error message from response data if hasattr(response, 'data'): if isinstance(response.data, dict): # DRF validation errors if 'detail' in response.data: error_message = str(response.data['detail']) elif 'non_field_errors' in response.data: error_message = str(response.data['non_field_errors'][0]) if response.data['non_field_errors'] else None errors = response.data else: # Field-specific errors errors = response.data # Create top-level error message if errors: first_error = list(errors.values())[0] if errors else None if first_error and isinstance(first_error, list) and len(first_error) > 0: error_message = str(first_error[0]) elif first_error: error_message = str(first_error) else: error_message = 'Validation failed' elif isinstance(response.data, list): # List of errors error_message = str(response.data[0]) if response.data else 'Validation failed' else: error_message = str(response.data) # Map status codes to appropriate error messages if not error_message: if status_code == status.HTTP_400_BAD_REQUEST: error_message = 'Bad request' elif status_code == status.HTTP_401_UNAUTHORIZED: error_message = 'Authentication required' elif status_code == status.HTTP_403_FORBIDDEN: error_message = 'Permission denied' elif status_code == status.HTTP_404_NOT_FOUND: error_message = 'Resource not found' elif status_code == status.HTTP_409_CONFLICT: error_message = 'Conflict' elif status_code == status.HTTP_422_UNPROCESSABLE_ENTITY: error_message = 'Validation failed' elif status_code == status.HTTP_429_TOO_MANY_REQUESTS: error_message = 'Rate limit exceeded' elif status_code >= 500: error_message = 'Internal server error' else: error_message = 'An error occurred' # Prepare debug info (only in DEBUG mode) debug_info = None if settings.DEBUG: debug_info = { 'exception_type': type(exc).__name__, 'exception_message': str(exc), 'view': context.get('view').__class__.__name__ if context.get('view') else None, 'path': request.path if request else None, 'method': request.method if request else None, } # Include traceback in debug mode import traceback debug_info['traceback'] = traceback.format_exc() # Log the error if status_code >= 500: logger.error( f"Server error: {error_message}", extra={ 'request_id': request_id, 'endpoint': request.path if request else None, 'method': request.method if request else None, 'user_id': request.user.id if request and request.user and request.user.is_authenticated else None, 'account_id': request.account.id if request and hasattr(request, 'account') and request.account else None, 'status_code': status_code, 'exception_type': type(exc).__name__, }, exc_info=True ) elif status_code >= 400: logger.warning( f"Client error: {error_message}", extra={ 'request_id': request_id, 'endpoint': request.path if request else None, 'method': request.method if request else None, 'user_id': request.user.id if request and request.user and request.user.is_authenticated else None, 'account_id': request.account.id if request and hasattr(request, 'account') and request.account else None, 'status_code': status_code, } ) # Return unified error response return error_response( error=error_message, errors=errors, status_code=status_code, request=request, debug_info=debug_info ) # If DRF didn't handle it, it's an unhandled exception # Log it and return unified error response logger.error( f"Unhandled exception: {type(exc).__name__}: {str(exc)}", extra={ 'request_id': request_id, 'endpoint': request.path if request else None, 'method': request.method if request else None, 'user_id': request.user.id if request and request.user and request.user.is_authenticated else None, 'account_id': request.account.id if request and hasattr(request, 'account') and request.account else None, }, exc_info=True ) # Prepare debug info debug_info = None if settings.DEBUG: import traceback debug_info = { 'exception_type': type(exc).__name__, 'exception_message': str(exc), 'view': context.get('view').__class__.__name__ if context.get('view') else None, 'path': request.path if request else None, 'method': request.method if request else None, 'traceback': traceback.format_exc() } return error_response( error='Internal server error', status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, request=request, debug_info=debug_info )