From 66b1868672928059d1e254e16c5ddc082e25883d Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Fri, 14 Nov 2025 15:50:26 +0000 Subject: [PATCH] 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. --- backend/igny8_core/api/__init__.py | 3 +- backend/igny8_core/api/exception_handlers.py | 213 +++++++++++++++++++ backend/igny8_core/settings.py | 1 + 3 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 backend/igny8_core/api/exception_handlers.py diff --git a/backend/igny8_core/api/__init__.py b/backend/igny8_core/api/__init__.py index 2cbd0659..314aa598 100644 --- a/backend/igny8_core/api/__init__.py +++ b/backend/igny8_core/api/__init__.py @@ -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'] diff --git a/backend/igny8_core/api/exception_handlers.py b/backend/igny8_core/api/exception_handlers.py new file mode 100644 index 00000000..9181e451 --- /dev/null +++ b/backend/igny8_core/api/exception_handlers.py @@ -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) + diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py index 9018e10f..db1cb6f8 100644 --- a/backend/igny8_core/settings.py +++ b/backend/igny8_core/settings.py @@ -204,6 +204,7 @@ REST_FRAMEWORK = { 'igny8_core.api.authentication.CSRFExemptSessionAuthentication', # Session auth without CSRF for API 'rest_framework.authentication.BasicAuthentication', # Enable basic auth as fallback ], + 'EXCEPTION_HANDLER': 'igny8_core.api.exception_handlers.custom_exception_handler', # Unified error format } # CORS Configuration