""" Custom exception handling for ThrillWiki API. Provides standardized error responses following Django styleguide patterns. """ 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