177 lines
7.1 KiB
Python
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
|
|
)
|
|
|
|
|