""" Custom exception handling for ThrillWiki API. Provides standardized error responses following Django styleguide patterns. """ import logging from typing import Any, Dict, Optional from django.http import Http404 from django.core.exceptions import PermissionDenied, ValidationError as DjangoValidationError from rest_framework import status from rest_framework.response import Response from rest_framework.views import exception_handler from rest_framework.exceptions import ValidationError as DRFValidationError, NotFound, PermissionDenied as DRFPermissionDenied from ..exceptions import ThrillWikiException from ..logging import get_logger, log_exception logger = get_logger(__name__) def custom_exception_handler(exc: Exception, context: Dict[str, Any]) -> Optional[Response]: """ Custom exception handler for DRF that provides standardized error responses. Returns: Response with standardized error format or None to fallback to default handler """ # Call REST framework's default exception handler first response = exception_handler(exc, context) if response is not None: # Standardize the error response format custom_response_data = { 'status': 'error', 'error': { 'code': _get_error_code(exc), 'message': _get_error_message(exc, response.data), 'details': _get_error_details(exc, response.data), }, 'data': None, } # Add request context for debugging if hasattr(context.get('request'), 'user'): custom_response_data['error']['request_user'] = str(context['request'].user) # Log the error for monitoring log_exception(logger, exc, context={'response_status': response.status_code}, request=context.get('request')) response.data = custom_response_data # Handle ThrillWiki custom exceptions elif isinstance(exc, ThrillWikiException): custom_response_data = { 'status': 'error', 'error': exc.to_dict(), 'data': None, } log_exception(logger, exc, context={'response_status': exc.status_code}, request=context.get('request')) response = Response(custom_response_data, status=exc.status_code) # Handle specific Django exceptions that DRF doesn't catch elif isinstance(exc, DjangoValidationError): custom_response_data = { 'status': 'error', 'error': { 'code': 'VALIDATION_ERROR', 'message': 'Validation failed', 'details': _format_django_validation_errors(exc), }, 'data': None, } log_exception(logger, exc, context={'response_status': status.HTTP_400_BAD_REQUEST}, request=context.get('request')) response = Response(custom_response_data, status=status.HTTP_400_BAD_REQUEST) elif isinstance(exc, Http404): custom_response_data = { 'status': 'error', 'error': { 'code': 'NOT_FOUND', 'message': 'Resource not found', 'details': str(exc) if str(exc) else None, }, 'data': None, } log_exception(logger, exc, context={'response_status': status.HTTP_404_NOT_FOUND}, request=context.get('request')) response = Response(custom_response_data, status=status.HTTP_404_NOT_FOUND) elif isinstance(exc, PermissionDenied): custom_response_data = { 'status': 'error', 'error': { 'code': 'PERMISSION_DENIED', 'message': 'Permission denied', 'details': str(exc) if str(exc) else None, }, 'data': None, } log_exception(logger, exc, context={'response_status': status.HTTP_403_FORBIDDEN}, request=context.get('request')) response = Response(custom_response_data, status=status.HTTP_403_FORBIDDEN) return response def _get_error_code(exc: Exception) -> str: """Extract or determine error code from exception.""" if hasattr(exc, 'default_code'): return exc.default_code.upper() if isinstance(exc, DRFValidationError): return 'VALIDATION_ERROR' elif isinstance(exc, NotFound): return 'NOT_FOUND' elif isinstance(exc, DRFPermissionDenied): return 'PERMISSION_DENIED' return exc.__class__.__name__.upper() def _get_error_message(exc: Exception, response_data: Any) -> str: """Extract user-friendly error message.""" if isinstance(response_data, dict): # Handle DRF validation errors if 'detail' in response_data: return str(response_data['detail']) elif 'non_field_errors' in response_data: errors = response_data['non_field_errors'] return errors[0] if isinstance(errors, list) and errors else str(errors) elif isinstance(response_data, dict) and len(response_data) == 1: key, value = next(iter(response_data.items())) if isinstance(value, list) and value: return f"{key}: {value[0]}" return f"{key}: {value}" # Fallback to exception message return str(exc) if str(exc) else 'An error occurred' def _get_error_details(exc: Exception, response_data: Any) -> Optional[Dict[str, Any]]: """Extract detailed error information for debugging.""" if isinstance(response_data, dict) and len(response_data) > 1: return response_data if hasattr(exc, 'detail') and isinstance(exc.detail, dict): return exc.detail return None def _format_django_validation_errors(exc: DjangoValidationError) -> Dict[str, Any]: """Format Django ValidationError for API response.""" if hasattr(exc, 'error_dict'): # Field-specific errors return { field: [str(error) for error in errors] for field, errors in exc.error_dict.items() } elif hasattr(exc, 'error_list'): # Non-field errors return { 'non_field_errors': [str(error) for error in exc.error_list] } return {'non_field_errors': [str(exc)]} # Removed _log_api_error - using centralized logging instead