- Add custom_exception_handler to handle exceptions in a unified format - Log errors with request context and provide debug information in development mode - Update settings.py to use the new exception handler - Export custom_exception_handler in __init__.py for accessibility This enhances error management across the API, improving debugging and user experience.
214 lines
7.2 KiB
Python
214 lines
7.2 KiB
Python
"""
|
|
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)
|
|
|