Files
igny8/backend/igny8_core/api/exception_handlers.py

177 lines
7.1 KiB
Python

"""
Centralized Exception Handler
Wraps all exceptions in unified format
"""
import logging
from rest_framework.views import exception_handler
from rest_framework import status
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from .response import get_request_id, error_response
logger = logging.getLogger(__name__)
def custom_exception_handler(exc, context):
"""
Custom exception handler that wraps all errors in unified format
Args:
exc: The exception that was raised
context: Dictionary containing request, view, args, kwargs
Returns:
Response object with unified error format
"""
# Get request from context
request = context.get('request')
# Get request ID
request_id = get_request_id(request) if request else None
# Call DRF's default exception handler first
response = exception_handler(exc, context)
# If DRF handled it, wrap it in unified format
if response is not None:
# Extract error details from DRF response
error_message = None
errors = None
status_code = response.status_code
# Try to extract error message from response data
if hasattr(response, 'data'):
if isinstance(response.data, dict):
# DRF validation errors
if 'detail' in response.data:
error_message = str(response.data['detail'])
elif 'non_field_errors' in response.data:
error_message = str(response.data['non_field_errors'][0]) if response.data['non_field_errors'] else None
errors = response.data
else:
# Field-specific errors
errors = response.data
# Create top-level error message
if errors:
first_error = list(errors.values())[0] if errors else None
if first_error and isinstance(first_error, list) and len(first_error) > 0:
error_message = str(first_error[0])
elif first_error:
error_message = str(first_error)
else:
error_message = 'Validation failed'
elif isinstance(response.data, list):
# List of errors
error_message = str(response.data[0]) if response.data else 'Validation failed'
else:
error_message = str(response.data)
# Map status codes to appropriate error messages
if not error_message:
if status_code == status.HTTP_400_BAD_REQUEST:
error_message = 'Bad request'
elif status_code == status.HTTP_401_UNAUTHORIZED:
error_message = 'Authentication required'
elif status_code == status.HTTP_403_FORBIDDEN:
error_message = 'Permission denied'
elif status_code == status.HTTP_404_NOT_FOUND:
error_message = 'Resource not found'
elif status_code == status.HTTP_409_CONFLICT:
error_message = 'Conflict'
elif status_code == status.HTTP_422_UNPROCESSABLE_ENTITY:
error_message = 'Validation failed'
elif status_code == status.HTTP_429_TOO_MANY_REQUESTS:
error_message = 'Rate limit exceeded'
elif status_code >= 500:
error_message = 'Internal server error'
else:
error_message = 'An error occurred'
# Prepare debug info (only in DEBUG mode)
debug_info = None
if settings.DEBUG:
debug_info = {
'exception_type': type(exc).__name__,
'exception_message': str(exc),
'view': context.get('view').__class__.__name__ if context.get('view') else None,
'path': request.path if request else None,
'method': request.method if request else None,
}
# Include traceback in debug mode
import traceback
debug_info['traceback'] = traceback.format_exc()
# Log the error
if status_code >= 500:
logger.error(
f"Server error: {error_message}",
extra={
'request_id': request_id,
'endpoint': request.path if request else None,
'method': request.method if request else None,
'user_id': request.user.id if request and request.user and request.user.is_authenticated else None,
'account_id': request.account.id if request and hasattr(request, 'account') and request.account else None,
'status_code': status_code,
'exception_type': type(exc).__name__,
},
exc_info=True
)
elif status_code >= 400:
logger.warning(
f"Client error: {error_message}",
extra={
'request_id': request_id,
'endpoint': request.path if request else None,
'method': request.method if request else None,
'user_id': request.user.id if request and request.user and request.user.is_authenticated else None,
'account_id': request.account.id if request and hasattr(request, 'account') and request.account else None,
'status_code': status_code,
}
)
# Return unified error response
return error_response(
error=error_message,
errors=errors,
status_code=status_code,
request=request,
debug_info=debug_info
)
# If DRF didn't handle it, it's an unhandled exception
# Log it and return unified error response
logger.error(
f"Unhandled exception: {type(exc).__name__}: {str(exc)}",
extra={
'request_id': request_id,
'endpoint': request.path if request else None,
'method': request.method if request else None,
'user_id': request.user.id if request and request.user and request.user.is_authenticated else None,
'account_id': request.account.id if request and hasattr(request, 'account') and request.account else None,
},
exc_info=True
)
# Prepare debug info
debug_info = None
if settings.DEBUG:
import traceback
debug_info = {
'exception_type': type(exc).__name__,
'exception_message': str(exc),
'view': context.get('view').__class__.__name__ if context.get('view') else None,
'path': request.path if request else None,
'method': request.method if request else None,
'traceback': traceback.format_exc()
}
return error_response(
error='Internal server error',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request,
debug_info=debug_info
)