mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:11:08 -05:00
- Cleaned up and standardized assertions in ApiTestMixin for API response validation. - Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE. - Removed unused imports and improved formatting in settings.py. - Refactored URL patterns in urls.py for better readability and organization. - Enhanced view functions in views.py for consistency and clarity. - Added .flake8 configuration for linting and style enforcement. - Introduced type stubs for django-environ to improve type checking with Pylance.
206 lines
6.4 KiB
Python
206 lines
6.4 KiB
Python
"""
|
|
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
|