| """ |
| Comprehensive Error Handling Middleware |
| Provides detailed error responses and logging for production use |
| """ |
|
|
| from datetime import datetime |
| import json |
| import logging |
| import traceback |
| from typing import Any, Dict, Optional |
| import uuid |
| from fastapi import HTTPException, Request, Response |
| from fastapi.responses import JSONResponse |
| from starlette.middleware.base import BaseHTTPMiddleware |
|
|
| |
| error_logger = logging.getLogger("atom.errors") |
| performance_logger = logging.getLogger("atom.performance") |
|
|
| class ErrorHandlingMiddleware(BaseHTTPMiddleware): |
| """Comprehensive error handling middleware""" |
|
|
| def __init__(self, app, debug: bool = False): |
| super().__init__(app) |
| self.debug = debug |
| self.setup_logging() |
|
|
| def setup_logging(self): |
| """Setup error logging configuration""" |
| |
| error_handler = logging.FileHandler("logs/errors.log") |
| error_handler.setLevel(logging.ERROR) |
| error_formatter = logging.Formatter( |
| '%(asctime)s - %(name)s - %(levelname)s - %(message)s' |
| ) |
| error_handler.setFormatter(error_formatter) |
| error_logger.addHandler(error_handler) |
|
|
| |
| perf_handler = logging.FileHandler("logs/performance.log") |
| perf_handler.setLevel(logging.INFO) |
| perf_handler.setFormatter(error_formatter) |
| performance_logger.addHandler(perf_handler) |
|
|
| async def dispatch(self, request: Request, call_next): |
| """Process request and handle any errors""" |
| |
| request_id = str(uuid.uuid4()) |
| start_time = datetime.now() |
|
|
| |
| request.state.request_id = request_id |
|
|
| try: |
| |
| response = await call_next(request) |
|
|
| |
| duration = (datetime.now() - start_time).total_seconds() |
| self.log_performance(request, response, duration, request_id) |
|
|
| |
| response.headers["X-Request-ID"] = request_id |
|
|
| return response |
|
|
| except HTTPException as e: |
| |
| return await self.handle_http_exception(e, request, request_id, start_time) |
|
|
| except Exception as e: |
| |
| return await self.handle_server_error(e, request, request_id, start_time) |
|
|
| async def handle_http_exception( |
| self, |
| exception: HTTPException, |
| request: Request, |
| request_id: str, |
| start_time: datetime |
| ) -> JSONResponse: |
| """Handle HTTP exceptions (4xx errors)""" |
|
|
| error_response = { |
| "error": { |
| "type": "http_error", |
| "code": exception.status_code, |
| "message": exception.detail, |
| "request_id": request_id, |
| "timestamp": datetime.now().isoformat(), |
| "path": str(request.url.path), |
| "method": request.method |
| } |
| } |
|
|
| |
| if self.debug: |
| error_response["debug"] = { |
| "headers": dict(request.headers), |
| "query_params": dict(request.query_params) |
| } |
|
|
| |
| error_logger.warning( |
| f"HTTP {exception.status_code} - {request.method} {request.url.path} - " |
| f"{exception.detail} - Request ID: {request_id}" |
| ) |
|
|
| return JSONResponse( |
| status_code=exception.status_code, |
| content=error_response |
| ) |
|
|
| async def handle_server_error( |
| self, |
| exception: Exception, |
| request: Request, |
| request_id: str, |
| start_time: datetime |
| ) -> JSONResponse: |
| """Handle server errors (5xx errors)""" |
|
|
| |
| error_traceback = traceback.format_exc() |
|
|
| |
| error_logger.error( |
| f"Server Error - {request.method} {request.url.path} - " |
| f"{str(exception)} - Request ID: {request_id}\n" |
| f"Traceback:\n{error_traceback}" |
| ) |
|
|
| |
| error_response = { |
| "error": { |
| "type": "server_error", |
| "code": 500, |
| "message": "Internal server error occurred", |
| "request_id": request_id, |
| "timestamp": datetime.now().isoformat(), |
| "path": str(request.url.path), |
| "method": request.method |
| } |
| } |
|
|
| |
| if self.debug: |
| error_response["debug"] = { |
| "exception": str(exception), |
| "traceback": error_traceback.split('\n'), |
| "headers": dict(request.headers) |
| } |
|
|
| return JSONResponse( |
| status_code=500, |
| content=error_response |
| ) |
|
|
| def log_performance( |
| self, |
| request: Request, |
| response: Response, |
| duration: float, |
| request_id: str |
| ): |
| """Log performance metrics""" |
| |
| if duration > 2.0: |
| performance_logger.warning( |
| f"Slow Request - {request.method} {request.url.path} - " |
| f"{duration:.3f}s - Status: {response.status_code} - " |
| f"Request ID: {request_id}" |
| ) |
| else: |
| performance_logger.info( |
| f"Request - {request.method} {request.url.path} - " |
| f"{duration:.3f}s - Status: {response.status_code} - " |
| f"Request ID: {request_id}" |
| ) |
|
|
|
|
| class ValidationErrorMiddleware(BaseHTTPMiddleware): |
| """Middleware for handling Pydantic validation errors""" |
|
|
| async def dispatch(self, request: Request, call_next): |
| try: |
| return await call_next(request) |
| except Exception as e: |
| |
| if "validation" in str(e).lower() or "pydantic" in str(e).lower(): |
| return self.handle_validation_error(e, request) |
| else: |
| |
| raise |
|
|
| def handle_validation_error(self, exception: Exception, request: Request) -> JSONResponse: |
| """Handle validation errors with detailed feedback""" |
|
|
| |
| validation_errors = [] |
|
|
| try: |
| |
| error_str = str(exception) |
|
|
| |
| if "field required" in error_str.lower(): |
| validation_errors.append({ |
| "field": "unknown", |
| "message": "Required field is missing", |
| "type": "missing" |
| }) |
|
|
| |
| |
|
|
| except Exception as e: |
| logger.warning(f"Failed to parse validation error detail: {e}") |
|
|
| error_response = { |
| "error": { |
| "type": "validation_error", |
| "code": 422, |
| "message": "Invalid request data", |
| "timestamp": datetime.now().isoformat(), |
| "path": str(request.url.path), |
| "method": request.method, |
| "validation_errors": validation_errors |
| } |
| } |
|
|
| return JSONResponse( |
| status_code=422, |
| content=error_response |
| ) |
|
|
|
|
| class CircuitBreakerMiddleware(BaseHTTPMiddleware): |
| """Simple circuit breaker for critical endpoints""" |
|
|
| def __init__(self, app, failure_threshold: int = 5, timeout: int = 60): |
| super().__init__(app) |
| self.failure_threshold = failure_threshold |
| self.timeout = timeout |
| self.failure_count = {} |
| self.last_failure_time = {} |
|
|
| async def dispatch(self, request: Request, call_next): |
| endpoint = f"{request.method}_{request.url.path}" |
|
|
| |
| if self.is_circuit_open(endpoint): |
| return JSONResponse( |
| status_code=503, |
| content={ |
| "error": { |
| "type": "service_unavailable", |
| "message": "Service temporarily unavailable. Please try again later.", |
| "retry_after": self.timeout |
| } |
| } |
| ) |
|
|
| try: |
| response = await call_next(request) |
|
|
| |
| if endpoint in self.failure_count: |
| del self.failure_count[endpoint] |
| if endpoint in self.last_failure_time: |
| del self.last_failure_time[endpoint] |
|
|
| return response |
|
|
| except Exception as e: |
| |
| self.failure_count[endpoint] = self.failure_count.get(endpoint, 0) + 1 |
| self.last_failure_time[endpoint] = datetime.now() |
|
|
| |
| if self.failure_count[endpoint] >= self.failure_threshold: |
| error_logger.critical( |
| f"Circuit breaker opened for endpoint: {endpoint} - " |
| f"Failure count: {self.failure_count[endpoint]}" |
| ) |
|
|
| raise |
|
|
|
|
| def setup_error_middleware(app, debug: bool = False): |
| """Setup all error handling middleware""" |
| |
| app.add_middleware(ValidationErrorMiddleware) |
| app.add_middleware(CircuitBreakerMiddleware) |
| app.add_middleware(ErrorHandlingMiddleware, debug=debug) |
|
|
| |
| import os |
| os.makedirs("logs", exist_ok=True) |