1208 lines
36 KiB
Markdown
1208 lines
36 KiB
Markdown
# API Implementation Plan - Section 3: Implement Consistent Error Handling
|
|
|
|
**Date:** 2025-01-XX
|
|
**Status:** Planning
|
|
**Priority:** High
|
|
**Related Document:** `API-ENDPOINTS-ANALYSIS.md`, `API-IMPLEMENTATION-PLAN-SECTION1.md`
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
This document outlines the implementation plan for **Section 3: Implement Consistent Error Handling** across the IGNY8 API layer. The goal is to ensure all exceptions, validation errors, and system-level failures return uniform, predictable JSON responses with proper HTTP status codes and user-readable messages.
|
|
|
|
**Key Objectives:**
|
|
- Create centralized exception handler that wraps all errors in unified format
|
|
- Unify validation error format across all endpoints
|
|
- Implement proper logging for debugging and monitoring
|
|
- Add developer-focused debug support for development environments
|
|
- Ensure consistent error responses for all failure scenarios
|
|
|
|
**Note:** This section builds on Section 1 Task 4 (Exception Handlers) and extends it with logging, debugging, and validation error unification.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Current State Analysis](#current-state-analysis)
|
|
2. [Implementation Tasks](#implementation-tasks)
|
|
3. [Task 1: Create Centralized Exception Handler](#task-1-create-centralized-exception-handler)
|
|
4. [Task 2: Register in Global DRF Settings](#task-2-register-in-global-drf-settings)
|
|
5. [Task 3: Unify Validation Error Format](#task-3-unify-validation-error-format)
|
|
6. [Task 4: Patch or Log Internal Server Errors](#task-4-patch-or-log-internal-server-errors)
|
|
7. [Task 5: Add Developer-Focused Debug Support](#task-5-add-developer-focused-debug-support)
|
|
8. [Task 6: Test Scenarios](#task-6-test-scenarios)
|
|
9. [Task 7: Changelog Entry](#task-7-changelog-entry)
|
|
10. [Testing Strategy](#testing-strategy)
|
|
11. [Rollout Plan](#rollout-plan)
|
|
12. [Success Criteria](#success-criteria)
|
|
|
|
---
|
|
|
|
## Current State Analysis
|
|
|
|
### Current Error Handling Issues
|
|
|
|
Based on `API-ENDPOINTS-ANALYSIS.md` and codebase analysis:
|
|
|
|
1. **Inconsistent Error Formats:**
|
|
- Some endpoints return `{ error: "..." }`
|
|
- Some return `{ success: false, message: "..." }`
|
|
- Some return DRF default format
|
|
- Validation errors may not be consistently formatted
|
|
|
|
2. **Exception Handling:**
|
|
- Section 1 Task 4 creates basic exception handler
|
|
- No logging for unhandled exceptions
|
|
- No request ID tracking for error correlation
|
|
- No debug information in development mode
|
|
|
|
3. **Validation Errors:**
|
|
- DRF serializer errors are handled automatically
|
|
- Manual validation errors may not follow unified format
|
|
- Field-specific errors may not be consistently structured
|
|
|
|
4. **Error Logging:**
|
|
- Some errors are logged, but not consistently
|
|
- No centralized error logging strategy
|
|
- No integration with error tracking services (Sentry, etc.)
|
|
|
|
5. **Frontend Error Handling:**
|
|
- Frontend has error parsing logic but may not handle all formats
|
|
- Error messages may not be user-friendly
|
|
- No consistent error display strategy
|
|
|
|
---
|
|
|
|
## Implementation Tasks
|
|
|
|
### Overview
|
|
|
|
| Task ID | Task Name | Priority | Estimated Effort | Dependencies |
|
|
|---------|-----------|----------|------------------|--------------|
|
|
| 1.1 | Create centralized exception handler | High | 4 hours | Section 1 Task 1 (response.py) |
|
|
| 1.2 | Enhance exception handler with logging | High | 3 hours | 1.1 |
|
|
| 2.1 | Register exception handler in settings | High | 1 hour | 1.1 |
|
|
| 3.1 | Audit validation error usage | Medium | 3 hours | None |
|
|
| 3.2 | Unify validation error format | High | 4 hours | 1.1 |
|
|
| 4.1 | Configure error logging | High | 3 hours | None |
|
|
| 4.2 | Add request ID tracking | Medium | 2 hours | 4.1 |
|
|
| 4.3 | Integrate error tracking service (optional) | Low | 4 hours | 4.1 |
|
|
| 5.1 | Add debug mode support | Low | 3 hours | 1.1 |
|
|
| 6.1 | Create test scenarios | High | 4 hours | All tasks |
|
|
| 7.1 | Create changelog entry | Low | 1 hour | All tasks |
|
|
|
|
**Total Estimated Effort:** ~32 hours
|
|
|
|
---
|
|
|
|
## Task 1: Create Centralized Exception Handler
|
|
|
|
### Goal
|
|
Create a comprehensive exception handler that wraps all exceptions in the unified error format with proper logging and debugging support.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 1.1: Create Enhanced Exception Handler
|
|
|
|
**File:** `backend/igny8_core/api/exception_handlers.py`
|
|
|
|
**Note:** This extends Section 1 Task 4 with enhanced error handling, logging, and debug support.
|
|
|
|
**Implementation:**
|
|
```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)
|
|
```
|
|
|
|
#### Step 1.2: Create Request ID Middleware (Optional but Recommended)
|
|
|
|
**File:** `backend/igny8_core/api/middleware.py`
|
|
|
|
**Implementation:**
|
|
```python
|
|
"""
|
|
Request ID Middleware for Error Tracking
|
|
"""
|
|
|
|
import uuid
|
|
from django.utils.deprecation import MiddlewareMixin
|
|
|
|
|
|
class RequestIDMiddleware(MiddlewareMixin):
|
|
"""
|
|
Middleware that adds a unique request ID to each request.
|
|
|
|
This request ID is used for error tracking and correlation.
|
|
"""
|
|
|
|
def process_request(self, request):
|
|
"""
|
|
Generate and attach request ID to request.
|
|
"""
|
|
# Check if request ID is already set (e.g., by reverse proxy)
|
|
request_id = request.META.get('HTTP_X_REQUEST_ID')
|
|
|
|
if not request_id:
|
|
# Generate new request ID
|
|
request_id = str(uuid.uuid4())
|
|
|
|
# Attach to request
|
|
request.request_id = request_id
|
|
|
|
# Add to response headers
|
|
# This will be done in process_response
|
|
|
|
def process_response(self, request, response):
|
|
"""
|
|
Add request ID to response headers.
|
|
"""
|
|
request_id = getattr(request, 'request_id', None)
|
|
if request_id:
|
|
response['X-Request-ID'] = request_id
|
|
|
|
return response
|
|
```
|
|
|
|
**Register in settings:**
|
|
```python
|
|
MIDDLEWARE = [
|
|
# ... other middleware ...
|
|
'igny8_core.api.middleware.RequestIDMiddleware',
|
|
# ... other middleware ...
|
|
]
|
|
```
|
|
|
|
**Estimated Time:** 4 hours (handler) + 2 hours (middleware) = 6 hours
|
|
|
|
---
|
|
|
|
## Task 2: Register in Global DRF Settings
|
|
|
|
### Goal
|
|
Register the custom exception handler in Django REST Framework settings.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 2.1: Update REST_FRAMEWORK Settings
|
|
|
|
**File:** `backend/igny8_core/settings.py`
|
|
|
|
**Update REST_FRAMEWORK configuration:**
|
|
```python
|
|
REST_FRAMEWORK = {
|
|
'DEFAULT_PAGINATION_CLASS': 'igny8_core.api.pagination.CustomPageNumberPagination',
|
|
'PAGE_SIZE': 10,
|
|
'DEFAULT_FILTER_BACKENDS': [
|
|
'django_filters.rest_framework.DjangoFilterBackend',
|
|
'rest_framework.filters.SearchFilter',
|
|
'rest_framework.filters.OrderingFilter',
|
|
],
|
|
'DEFAULT_PERMISSION_CLASSES': [
|
|
'rest_framework.permissions.AllowAny', # Will be changed in Section 2
|
|
],
|
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
|
'igny8_core.api.authentication.JWTAuthentication',
|
|
'igny8_core.api.authentication.CSRFExemptSessionAuthentication',
|
|
'rest_framework.authentication.BasicAuthentication',
|
|
],
|
|
'EXCEPTION_HANDLER': 'igny8_core.api.exception_handlers.custom_exception_handler', # Add this
|
|
}
|
|
```
|
|
|
|
**Estimated Time:** 1 hour
|
|
|
|
---
|
|
|
|
## Task 3: Unify Validation Error Format
|
|
|
|
### Goal
|
|
Ensure all validation errors (serializer and manual) use the unified error format.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 3.1: Audit Validation Error Usage
|
|
|
|
**Action Items:**
|
|
1. Search for manual validation in ViewSets
|
|
2. Identify places where `error_response` is not used
|
|
3. Document current validation error patterns
|
|
4. Create refactoring checklist
|
|
|
|
**Files to Audit:**
|
|
- All ViewSets with custom `create`, `update`, or `@action` methods
|
|
- Custom validation logic in serializers
|
|
- Manual validation in views
|
|
|
|
**Estimated Time:** 3 hours
|
|
|
|
#### Step 3.2: Unify Validation Error Format
|
|
|
|
**DRF Serializer Errors:**
|
|
DRF automatically handles serializer validation errors and passes them to the exception handler. The exception handler (Task 1) will wrap them in the unified format.
|
|
|
|
**Manual Validation Errors:**
|
|
|
|
**Before:**
|
|
```python
|
|
class KeywordViewSet(BaseTenantViewSet):
|
|
@action(detail=False, methods=['post'])
|
|
def bulk_delete(self, request):
|
|
ids = request.data.get('ids', [])
|
|
if not ids:
|
|
return Response({
|
|
"error": "No IDs provided"
|
|
}, status=400)
|
|
```
|
|
|
|
**After:**
|
|
```python
|
|
from igny8_core.api.response import error_response
|
|
|
|
class KeywordViewSet(BaseTenantViewSet):
|
|
@action(detail=False, methods=['post'])
|
|
def bulk_delete(self, request):
|
|
ids = request.data.get('ids', [])
|
|
if not ids:
|
|
return error_response(
|
|
error="No IDs provided",
|
|
errors={"ids": ["This field is required"]},
|
|
status_code=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
```
|
|
|
|
**Serializer Validation Override (if needed):**
|
|
|
|
**File:** `backend/igny8_core/api/serializers.py` (create if needed)
|
|
|
|
```python
|
|
"""
|
|
Base Serializer with Unified Error Format
|
|
"""
|
|
|
|
from rest_framework import serializers
|
|
from rest_framework.exceptions import ValidationError
|
|
|
|
|
|
class BaseSerializer(serializers.Serializer):
|
|
"""
|
|
Base serializer that ensures validation errors follow unified format.
|
|
|
|
DRF's default validation error handling should work with the exception handler,
|
|
but this can be used if custom validation error formatting is needed.
|
|
"""
|
|
|
|
def validate(self, attrs):
|
|
"""
|
|
Override to add custom validation logic.
|
|
"""
|
|
return attrs
|
|
|
|
def to_internal_value(self, data):
|
|
"""
|
|
Override to customize validation error format if needed.
|
|
"""
|
|
try:
|
|
return super().to_internal_value(data)
|
|
except ValidationError as e:
|
|
# Validation errors are already in the correct format
|
|
# The exception handler will wrap them
|
|
raise
|
|
|
|
|
|
class BaseModelSerializer(serializers.ModelSerializer, BaseSerializer):
|
|
"""
|
|
Base model serializer with unified error format support.
|
|
"""
|
|
pass
|
|
```
|
|
|
|
**Estimated Time:** 4 hours
|
|
|
|
---
|
|
|
|
## Task 4: Patch or Log Internal Server Errors
|
|
|
|
### Goal
|
|
Implement comprehensive error logging for debugging and monitoring.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 4.1: Configure Error Logging
|
|
|
|
**File:** `backend/igny8_core/settings.py`
|
|
|
|
**Add/Update LOGGING configuration:**
|
|
```python
|
|
LOGGING = {
|
|
'version': 1,
|
|
'disable_existing_loggers': False,
|
|
'formatters': {
|
|
'verbose': {
|
|
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
|
|
'style': '{',
|
|
},
|
|
'detailed': {
|
|
'format': '{levelname} {asctime} [{request_id}] {module} {path} {method} {status_code} {message}',
|
|
'style': '{',
|
|
},
|
|
},
|
|
'filters': {
|
|
'require_debug_false': {
|
|
'()': 'django.utils.log.RequireDebugFalse',
|
|
},
|
|
},
|
|
'handlers': {
|
|
'console': {
|
|
'class': 'logging.StreamHandler',
|
|
'formatter': 'verbose',
|
|
},
|
|
'file': {
|
|
'class': 'logging.handlers.RotatingFileHandler',
|
|
'filename': os.path.join(BASE_DIR, 'logs', 'django.log'),
|
|
'maxBytes': 1024 * 1024 * 10, # 10 MB
|
|
'backupCount': 5,
|
|
'formatter': 'detailed',
|
|
},
|
|
'error_file': {
|
|
'class': 'logging.handlers.RotatingFileHandler',
|
|
'filename': os.path.join(BASE_DIR, 'logs', 'errors.log'),
|
|
'maxBytes': 1024 * 1024 * 10, # 10 MB
|
|
'backupCount': 10,
|
|
'formatter': 'detailed',
|
|
'level': 'ERROR',
|
|
},
|
|
},
|
|
'loggers': {
|
|
'django': {
|
|
'handlers': ['console', 'file'],
|
|
'level': 'INFO',
|
|
'propagate': False,
|
|
},
|
|
'igny8_core.api': {
|
|
'handlers': ['console', 'file', 'error_file'],
|
|
'level': 'INFO',
|
|
'propagate': False,
|
|
},
|
|
'django.request': {
|
|
'handlers': ['error_file'],
|
|
'level': 'ERROR',
|
|
'propagate': False,
|
|
},
|
|
},
|
|
'root': {
|
|
'handlers': ['console'],
|
|
'level': 'INFO',
|
|
},
|
|
}
|
|
```
|
|
|
|
**Create logs directory:**
|
|
```bash
|
|
mkdir -p logs
|
|
touch logs/django.log logs/errors.log
|
|
```
|
|
|
|
**Estimated Time:** 3 hours
|
|
|
|
#### Step 4.2: Add Request ID Tracking
|
|
|
|
**Implementation:**
|
|
- Request ID middleware (Task 1.2) already adds request IDs
|
|
- Exception handler (Task 1.1) already logs request IDs
|
|
- Ensure request ID is included in all error responses
|
|
|
|
**Estimated Time:** Already included in Task 1
|
|
|
|
#### Step 4.3: Integrate Error Tracking Service (Optional)
|
|
|
|
**Sentry Integration Example:**
|
|
|
|
**Install Sentry:**
|
|
```bash
|
|
pip install sentry-sdk
|
|
```
|
|
|
|
**File:** `backend/igny8_core/settings.py`
|
|
|
|
**Add Sentry configuration:**
|
|
```python
|
|
import sentry_sdk
|
|
from sentry_sdk.integrations.django import DjangoIntegration
|
|
from sentry_sdk.integrations.logging import LoggingIntegration
|
|
|
|
# Only enable Sentry in production
|
|
if not settings.DEBUG and os.getenv('SENTRY_DSN'):
|
|
sentry_sdk.init(
|
|
dsn=os.getenv('SENTRY_DSN'),
|
|
integrations=[
|
|
DjangoIntegration(),
|
|
LoggingIntegration(level=logging.INFO, event_level=logging.ERROR),
|
|
],
|
|
traces_sample_rate=0.1, # 10% of transactions
|
|
send_default_pii=True, # Include user information
|
|
environment=os.getenv('ENVIRONMENT', 'production'),
|
|
)
|
|
```
|
|
|
|
**Update Exception Handler to Send to Sentry:**
|
|
|
|
**File:** `backend/igny8_core/api/exception_handlers.py`
|
|
|
|
**Add Sentry integration:**
|
|
```python
|
|
def custom_exception_handler(exc, context):
|
|
# ... existing code ...
|
|
|
|
# Send to Sentry for server errors
|
|
if status_code >= 500:
|
|
try:
|
|
import sentry_sdk
|
|
with sentry_sdk.push_scope() as scope:
|
|
scope.set_tag("request_id", request_id)
|
|
scope.set_tag("status_code", status_code)
|
|
scope.set_context("request", {
|
|
"path": request.path if request else None,
|
|
"method": request.method if request else None,
|
|
"view": view.__class__.__name__ if view else None,
|
|
})
|
|
sentry_sdk.capture_exception(exc)
|
|
except ImportError:
|
|
# Sentry not installed
|
|
pass
|
|
|
|
# ... rest of handler ...
|
|
```
|
|
|
|
**Estimated Time:** 4 hours (optional)
|
|
|
|
---
|
|
|
|
## Task 5: Add Developer-Focused Debug Support
|
|
|
|
### Goal
|
|
Add debug information to error responses in development mode for easier debugging.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 5.1: Enhance Exception Handler with Debug Mode
|
|
|
|
**Implementation:**
|
|
The exception handler (Task 1.1) already includes debug information when `settings.DEBUG` is `True`. This includes:
|
|
- Exception type and message
|
|
- View class name
|
|
- Request path and method
|
|
- Full traceback for server errors
|
|
|
|
**Additional Debug Features (Optional):**
|
|
|
|
**File:** `backend/igny8_core/api/exception_handlers.py`
|
|
|
|
**Add environment variable control:**
|
|
```python
|
|
def custom_exception_handler(exc, context):
|
|
# ... existing code ...
|
|
|
|
# Add debug information in development mode
|
|
# Can be controlled by DEBUG setting or API_DEBUG environment variable
|
|
api_debug = settings.DEBUG or os.getenv('API_DEBUG', 'False').lower() == 'true'
|
|
|
|
if api_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,
|
|
"user": str(request.user) if request and hasattr(request, 'user') else None,
|
|
"account": str(request.account) if request and hasattr(request, 'account') else None,
|
|
}
|
|
|
|
# Include traceback in debug mode for server errors
|
|
if status_code >= 500:
|
|
error_response_data["debug"]["traceback"] = traceback.format_exc()
|
|
|
|
# Include request data (sanitized)
|
|
if request:
|
|
error_response_data["debug"]["request_data"] = {
|
|
"GET": dict(request.GET),
|
|
"POST": {k: str(v)[:100] for k, v in dict(request.POST).items()}, # Truncate long values
|
|
}
|
|
|
|
# ... rest of handler ...
|
|
```
|
|
|
|
**Security Note:**
|
|
- Only include debug information when `DEBUG=True` or `API_DEBUG=True`
|
|
- Never include sensitive data (passwords, tokens) in debug output
|
|
- Sanitize request data before including in debug response
|
|
|
|
**Estimated Time:** 3 hours
|
|
|
|
---
|
|
|
|
## Task 6: Test Scenarios
|
|
|
|
### Goal
|
|
Create comprehensive test scenarios to validate error handling works correctly.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 6.1: Create Test Scenarios
|
|
|
|
**Test Cases:**
|
|
|
|
**1. Missing Required Field (400)**
|
|
```python
|
|
# Test: POST /api/v1/planner/keywords/ without required fields
|
|
# Expected: 400 Bad Request
|
|
{
|
|
"success": false,
|
|
"error": "keyword: This field is required",
|
|
"errors": {
|
|
"keyword": ["This field is required"],
|
|
"site_id": ["This field is required"]
|
|
},
|
|
"request_id": "abc-123-def"
|
|
}
|
|
```
|
|
|
|
**2. Invalid Token (401)**
|
|
```python
|
|
# Test: GET /api/v1/auth/me/ with invalid token
|
|
# Expected: 401 Unauthorized
|
|
{
|
|
"success": false,
|
|
"error": "Authentication required",
|
|
"request_id": "abc-123-def"
|
|
}
|
|
```
|
|
|
|
**3. Permission Denied (403)**
|
|
```python
|
|
# Test: DELETE /api/v1/auth/users/1/ as non-admin user
|
|
# Expected: 403 Forbidden
|
|
{
|
|
"success": false,
|
|
"error": "Permission denied",
|
|
"request_id": "abc-123-def"
|
|
}
|
|
```
|
|
|
|
**4. Not Found (404)**
|
|
```python
|
|
# Test: GET /api/v1/planner/keywords/99999/ (non-existent ID)
|
|
# Expected: 404 Not Found
|
|
{
|
|
"success": false,
|
|
"error": "Resource not found",
|
|
"request_id": "abc-123-def"
|
|
}
|
|
```
|
|
|
|
**5. Server Crash (500)**
|
|
```python
|
|
# Test: Endpoint that raises unhandled exception
|
|
# Expected: 500 Internal Server Error
|
|
{
|
|
"success": false,
|
|
"error": "Internal server error",
|
|
"request_id": "abc-123-def",
|
|
"debug": { # Only in DEBUG mode
|
|
"exception_type": "ValueError",
|
|
"exception_message": "...",
|
|
"traceback": "..."
|
|
}
|
|
}
|
|
```
|
|
|
|
**6. Manual Check Failure**
|
|
```python
|
|
# Test: Custom action with manual validation
|
|
# Expected: Custom error message
|
|
{
|
|
"success": false,
|
|
"error": "No IDs provided",
|
|
"errors": {
|
|
"ids": ["This field is required"]
|
|
},
|
|
"request_id": "abc-123-def"
|
|
}
|
|
```
|
|
|
|
**7. Validation Error with Multiple Fields**
|
|
```python
|
|
# Test: POST with multiple validation errors
|
|
# Expected: All errors in errors dict
|
|
{
|
|
"success": false,
|
|
"error": "email: Invalid email format",
|
|
"errors": {
|
|
"email": ["Invalid email format"],
|
|
"password": ["Password must be at least 8 characters"],
|
|
"password_confirm": ["Passwords do not match"]
|
|
},
|
|
"request_id": "abc-123-def"
|
|
}
|
|
```
|
|
|
|
#### Step 6.2: Create Automated Tests
|
|
|
|
**File:** `backend/igny8_core/api/tests/test_exception_handlers.py`
|
|
|
|
**Test Implementation:**
|
|
```python
|
|
"""
|
|
Tests for Exception Handler
|
|
"""
|
|
|
|
from django.test import TestCase
|
|
from rest_framework.test import APIClient
|
|
from rest_framework import status
|
|
from django.contrib.auth import get_user_model
|
|
from igny8_core.auth.models import Account
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class ExceptionHandlerTestCase(TestCase):
|
|
def setUp(self):
|
|
self.client = APIClient()
|
|
self.account = Account.objects.create(name="Test Account")
|
|
self.user = User.objects.create_user(
|
|
email="test@example.com",
|
|
password="testpass123",
|
|
account=self.account
|
|
)
|
|
|
|
def test_validation_error_format(self):
|
|
"""Test that validation errors return unified format"""
|
|
response = self.client.post('/api/v1/planner/keywords/', {})
|
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
data = response.json()
|
|
self.assertFalse(data['success'])
|
|
self.assertIn('error', data)
|
|
self.assertIn('errors', data)
|
|
self.assertIn('request_id', data)
|
|
|
|
def test_unauthorized_error_format(self):
|
|
"""Test that 401 errors return unified format"""
|
|
response = self.client.get('/api/v1/auth/me/')
|
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
|
data = response.json()
|
|
self.assertFalse(data['success'])
|
|
self.assertEqual(data['error'], 'Authentication required')
|
|
|
|
def test_not_found_error_format(self):
|
|
"""Test that 404 errors return unified format"""
|
|
self.client.force_authenticate(user=self.user)
|
|
response = self.client.get('/api/v1/planner/keywords/99999/')
|
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
data = response.json()
|
|
self.assertFalse(data['success'])
|
|
self.assertEqual(data['error'], 'Resource not found')
|
|
|
|
def test_request_id_present(self):
|
|
"""Test that request ID is included in error responses"""
|
|
response = self.client.post('/api/v1/planner/keywords/', {})
|
|
data = response.json()
|
|
self.assertIn('request_id', data)
|
|
self.assertIsNotNone(data['request_id'])
|
|
|
|
def test_debug_info_in_development(self):
|
|
"""Test that debug info is included when DEBUG=True"""
|
|
from django.conf import settings
|
|
original_debug = settings.DEBUG
|
|
settings.DEBUG = True
|
|
|
|
try:
|
|
# Trigger a server error
|
|
response = self.client.get('/api/v1/nonexistent-endpoint/')
|
|
if response.status_code >= 500:
|
|
data = response.json()
|
|
if 'debug' in data:
|
|
self.assertIn('exception_type', data['debug'])
|
|
finally:
|
|
settings.DEBUG = original_debug
|
|
```
|
|
|
|
**Estimated Time:** 4 hours
|
|
|
|
---
|
|
|
|
## Task 7: Changelog Entry
|
|
|
|
### Goal
|
|
Document the error handling standardization changes.
|
|
|
|
### Implementation Steps
|
|
|
|
#### Step 7.1: Update CHANGELOG.md
|
|
|
|
**File:** `CHANGELOG.md` (or similar)
|
|
|
|
**Add Entry:**
|
|
```markdown
|
|
## [Unreleased] - 2025-01-XX
|
|
|
|
### Changed
|
|
- **Error Handling**: Added centralized exception handler and standardized all API error formats
|
|
- Created `custom_exception_handler` in `igny8_core/api/exception_handlers.py`
|
|
- All exceptions now return unified error format: `{ success: false, error: "...", errors: {...}, request_id: "..." }`
|
|
- Added request ID tracking for error correlation
|
|
- Enhanced error logging with request context
|
|
- Added debug information in development mode (exception type, traceback, request details)
|
|
- Unified validation error format across all endpoints
|
|
|
|
### Added
|
|
- **Request ID Middleware**: Added `RequestIDMiddleware` for error tracking and correlation
|
|
- **Error Logging**: Configured comprehensive error logging to files and console
|
|
- **Debug Support**: Added debug information in error responses when `DEBUG=True`
|
|
- Includes exception type, message, view, path, method
|
|
- Includes full traceback for server errors (500+)
|
|
- Includes sanitized request data
|
|
|
|
### Security
|
|
- Debug information only included when `DEBUG=True` or `API_DEBUG=True`
|
|
- Sensitive data (passwords, tokens) excluded from debug output
|
|
- Request data sanitized before inclusion in debug responses
|
|
|
|
### Affected Areas
|
|
- API Layer (`igny8_core/api/exception_handlers.py`, `igny8_core/api/middleware.py`)
|
|
- All API endpoints (error responses now unified)
|
|
- Error logging configuration (`settings.py`)
|
|
|
|
### Migration Guide
|
|
1. **Frontend Updates:**
|
|
- Update error handling to check `success: false` field
|
|
- Use `error` field for top-level error message
|
|
- Use `errors` field for field-specific validation errors
|
|
- Use `request_id` for error reporting and support
|
|
|
|
2. **Error Response Format:**
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": "Top-level error message",
|
|
"errors": {
|
|
"field_name": ["Field-specific error messages"]
|
|
},
|
|
"request_id": "uuid-for-tracking",
|
|
"debug": { // Only in DEBUG mode
|
|
"exception_type": "ValidationError",
|
|
"traceback": "..."
|
|
}
|
|
}
|
|
```
|
|
|
|
3. **Testing:**
|
|
- All error responses now follow unified format
|
|
- Request IDs are included for error tracking
|
|
- Debug information available in development mode
|
|
```
|
|
|
|
**Estimated Time:** 1 hour
|
|
|
|
---
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests
|
|
|
|
**File:** `backend/igny8_core/api/tests/test_exception_handlers.py`
|
|
|
|
**Test Cases:**
|
|
- Exception handler wraps DRF exceptions correctly
|
|
- Error message extraction from various exception types
|
|
- Request ID generation and inclusion
|
|
- Debug information inclusion in development mode
|
|
- Status code mapping to error messages
|
|
- Validation error format handling
|
|
|
|
### Integration Tests
|
|
|
|
**Test Cases:**
|
|
- End-to-end API calls that trigger various error scenarios
|
|
- Error response format validation
|
|
- Request ID propagation
|
|
- Error logging verification
|
|
- Debug information in development mode
|
|
|
|
### Manual Testing
|
|
|
|
**Checklist:**
|
|
- [ ] Validation errors return unified format
|
|
- [ ] Authentication errors return unified format
|
|
- [ ] Permission errors return unified format
|
|
- [ ] Not found errors return unified format
|
|
- [ ] Server errors return unified format
|
|
- [ ] Request ID is included in all error responses
|
|
- [ ] Error logging works correctly
|
|
- [ ] Debug information appears in development mode
|
|
- [ ] Debug information is excluded in production mode
|
|
- [ ] Sensitive data is not included in debug output
|
|
|
|
---
|
|
|
|
## Rollout Plan
|
|
|
|
### Phase 1: Foundation (Week 1)
|
|
- ✅ Task 1: Create centralized exception handler
|
|
- ✅ Task 2: Register exception handler in settings
|
|
- ✅ Unit tests for exception handler
|
|
|
|
### Phase 2: Logging & Debug (Week 2)
|
|
- ✅ Task 4: Configure error logging
|
|
- ✅ Task 5: Add debug support
|
|
- ✅ Task 1.2: Add request ID middleware (optional)
|
|
|
|
### Phase 3: Validation & Testing (Week 3)
|
|
- ✅ Task 3: Unify validation error format
|
|
- ✅ Task 6: Create test scenarios
|
|
- ✅ Integration testing
|
|
|
|
### Phase 4: Documentation & Release (Week 4)
|
|
- ✅ Task 7: Changelog entry
|
|
- ✅ Documentation updates
|
|
- ✅ Release to staging
|
|
- ✅ Production deployment
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
### Definition of Done
|
|
|
|
1. ✅ Centralized exception handler is created and registered
|
|
2. ✅ All exceptions return unified error format
|
|
3. ✅ Request ID tracking is implemented
|
|
4. ✅ Error logging is configured and working
|
|
5. ✅ Debug information is available in development mode
|
|
6. ✅ Validation errors use unified format
|
|
7. ✅ All test scenarios pass
|
|
8. ✅ Documentation is updated
|
|
9. ✅ Changelog entry is created
|
|
10. ✅ No sensitive data in debug output
|
|
|
|
### Metrics
|
|
|
|
- **Coverage:** 100% of exceptions use unified format
|
|
- **Logging:** All errors are logged with request context
|
|
- **Test Coverage:** >90% for exception handler
|
|
- **Debug Support:** Debug information available in development mode
|
|
|
|
---
|
|
|
|
## Risk Assessment
|
|
|
|
### Risks
|
|
|
|
1. **Debug Information Leakage:** Debug info might leak sensitive data
|
|
- **Mitigation:** Only include debug info when `DEBUG=True`, sanitize request data
|
|
|
|
2. **Performance Impact:** Exception handling and logging may add overhead
|
|
- **Mitigation:** Minimal overhead, use async logging if needed
|
|
|
|
3. **Breaking Changes:** Frontend may expect different error format
|
|
- **Mitigation:** Document new format, provide migration guide
|
|
|
|
4. **Log File Growth:** Error logs may grow large
|
|
- **Mitigation:** Use rotating file handlers, set up log rotation
|
|
|
|
### Rollback Plan
|
|
|
|
If issues arise:
|
|
1. Revert exception handler registration (use DRF default)
|
|
2. Keep response format utilities (non-breaking)
|
|
3. Disable debug mode if sensitive data is exposed
|
|
4. Document issues for future fixes
|
|
|
|
---
|
|
|
|
## Appendix
|
|
|
|
### Error Response Format Examples
|
|
|
|
#### Validation Error (400)
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": "email: Invalid email format",
|
|
"errors": {
|
|
"email": ["Invalid email format"],
|
|
"password": ["Password must be at least 8 characters"]
|
|
},
|
|
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
}
|
|
```
|
|
|
|
#### Authentication Error (401)
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": "Authentication required",
|
|
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
}
|
|
```
|
|
|
|
#### Permission Error (403)
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": "Permission denied",
|
|
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
}
|
|
```
|
|
|
|
#### Not Found Error (404)
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": "Resource not found",
|
|
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
}
|
|
```
|
|
|
|
#### Server Error (500) - Production
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": "Internal server error",
|
|
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
}
|
|
```
|
|
|
|
#### Server Error (500) - Development
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": "Internal server error",
|
|
"request_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"debug": {
|
|
"exception_type": "ValueError",
|
|
"exception_message": "Invalid value",
|
|
"view": "KeywordViewSet",
|
|
"path": "/api/v1/planner/keywords/",
|
|
"method": "POST",
|
|
"traceback": "Traceback (most recent call last):\n ..."
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
**Document Status:** Implementation Plan
|
|
**Last Updated:** 2025-01-XX
|
|
**Next Review:** After Phase 1 completion
|
|
|