feat(api): implement centralized exception handling for unified error responses
- 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.
This commit is contained in:
@@ -3,6 +3,7 @@ IGNY8 API Module
|
||||
"""
|
||||
|
||||
from .response import success_response, error_response, paginated_response
|
||||
from .exception_handlers import custom_exception_handler
|
||||
|
||||
__all__ = ['success_response', 'error_response', 'paginated_response']
|
||||
__all__ = ['success_response', 'error_response', 'paginated_response', 'custom_exception_handler']
|
||||
|
||||
|
||||
213
backend/igny8_core/api/exception_handlers.py
Normal file
213
backend/igny8_core/api/exception_handlers.py
Normal file
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user