Coverage for netrun_errors / handlers.py: 75%
56 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-15 18:37 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-15 18:37 +0000
1"""
2Global exception handlers for FastAPI applications.
4Provides centralized exception handling with structured JSON responses,
5correlation ID injection, and logging integration.
7v1.1.0: Enhanced logging with optional netrun-logging integration for
8structured key=value logging and correlation ID consistency.
9"""
11import logging
12from typing import Union
14from fastapi import FastAPI, Request, status
15from fastapi.exceptions import RequestValidationError
16from fastapi.responses import JSONResponse
17from starlette.exceptions import HTTPException as StarletteHTTPException
19from .base import NetrunException
21# Try to use netrun-logging for structured logging
22_use_structlog = False
23_structlog_logger = None
25try:
26 from netrun_logging import get_logger as _get_structlog_logger
27 _structlog_logger = _get_structlog_logger(__name__)
28 _use_structlog = True
29except ImportError:
30 pass
32# Fallback to standard logging
33logger = logging.getLogger(__name__)
36def _log_error(
37 message: str,
38 level: str = "error",
39 exc_info: bool = False,
40 **kwargs
41) -> None:
42 """Log error using netrun-logging if available, otherwise standard logging."""
43 if _use_structlog and _structlog_logger is not None:
44 log_method = getattr(_structlog_logger, level, _structlog_logger.error)
45 log_method(message, **kwargs)
46 else:
47 log_method = getattr(logger, level, logger.error)
48 log_method(message, extra=kwargs, exc_info=exc_info)
51async def netrun_exception_handler(
52 request: Request, exc: NetrunException
53) -> JSONResponse:
54 """
55 Handle NetrunException instances with structured JSON responses.
57 Args:
58 request: FastAPI request object
59 exc: NetrunException instance
61 Returns:
62 JSONResponse with structured error format
63 """
64 # Add request path to error details
65 error_dict = exc.to_dict()
66 error_dict["error"]["path"] = str(request.url.path)
68 # Log error with correlation ID (structured logging if netrun-logging available)
69 _log_error(
70 f"netrun_exception: {exc.message}",
71 level="error",
72 correlation_id=exc.correlation_id,
73 error_code=exc.error_code,
74 status_code=exc.status_code,
75 path=str(request.url.path),
76 method=request.method,
77 )
79 return JSONResponse(
80 status_code=exc.status_code,
81 content=error_dict,
82 )
85async def validation_exception_handler(
86 request: Request, exc: RequestValidationError
87) -> JSONResponse:
88 """
89 Handle FastAPI request validation errors with structured format.
91 Args:
92 request: FastAPI request object
93 exc: RequestValidationError instance
95 Returns:
96 JSONResponse with validation error details
97 """
98 from datetime import datetime, timezone
100 correlation_id = NetrunException._generate_correlation_id()
102 error_response = {
103 "error": {
104 "code": "VALIDATION_ERROR",
105 "message": "Request validation failed",
106 "details": {
107 "validation_errors": exc.errors(),
108 },
109 "correlation_id": correlation_id,
110 "timestamp": datetime.now(timezone.utc).isoformat(),
111 "path": str(request.url.path),
112 }
113 }
115 _log_error(
116 "validation_error",
117 level="warning",
118 correlation_id=correlation_id,
119 path=str(request.url.path),
120 method=request.method,
121 error_count=len(exc.errors()),
122 )
124 return JSONResponse(
125 status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
126 content=error_response,
127 )
130async def http_exception_handler(
131 request: Request, exc: Union[StarletteHTTPException, Exception]
132) -> JSONResponse:
133 """
134 Handle generic HTTP exceptions with structured format.
136 Args:
137 request: FastAPI request object
138 exc: HTTPException or generic Exception
140 Returns:
141 JSONResponse with structured error format
142 """
143 from datetime import datetime, timezone
145 correlation_id = NetrunException._generate_correlation_id()
147 # Determine status code
148 if isinstance(exc, StarletteHTTPException):
149 status_code = exc.status_code
150 message = exc.detail if isinstance(exc.detail, str) else str(exc.detail)
151 else:
152 status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
153 message = "An unexpected error occurred"
155 error_response = {
156 "error": {
157 "code": "HTTP_ERROR",
158 "message": message,
159 "details": {},
160 "correlation_id": correlation_id,
161 "timestamp": datetime.now(timezone.utc).isoformat(),
162 "path": str(request.url.path),
163 }
164 }
166 _log_error(
167 f"http_exception: {message}",
168 level="error",
169 exc_info=(status_code >= 500),
170 correlation_id=correlation_id,
171 status_code=status_code,
172 path=str(request.url.path),
173 method=request.method,
174 )
176 return JSONResponse(
177 status_code=status_code,
178 content=error_response,
179 )
182async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
183 """
184 Handle unhandled exceptions with structured format.
186 Args:
187 request: FastAPI request object
188 exc: Exception instance
190 Returns:
191 JSONResponse with generic error format
192 """
193 from datetime import datetime, timezone
195 correlation_id = NetrunException._generate_correlation_id()
197 error_response = {
198 "error": {
199 "code": "INTERNAL_SERVER_ERROR",
200 "message": "An unexpected error occurred. Please try again later.",
201 "details": {},
202 "correlation_id": correlation_id,
203 "timestamp": datetime.now(timezone.utc).isoformat(),
204 "path": str(request.url.path),
205 }
206 }
208 _log_error(
209 "unhandled_exception",
210 level="error",
211 exc_info=True,
212 correlation_id=correlation_id,
213 path=str(request.url.path),
214 method=request.method,
215 exception_type=type(exc).__name__,
216 exception_message=str(exc),
217 )
219 return JSONResponse(
220 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
221 content=error_response,
222 )
225def install_exception_handlers(app: FastAPI) -> None:
226 """
227 Install all Netrun exception handlers on a FastAPI application.
229 Usage:
230 from fastapi import FastAPI
231 from netrun_errors import install_exception_handlers
233 app = FastAPI()
234 install_exception_handlers(app)
236 Args:
237 app: FastAPI application instance
238 """
239 app.add_exception_handler(NetrunException, netrun_exception_handler)
240 app.add_exception_handler(RequestValidationError, validation_exception_handler)
241 app.add_exception_handler(StarletteHTTPException, http_exception_handler)
242 app.add_exception_handler(Exception, unhandled_exception_handler)
244 _log_error(
245 "exception_handlers_installed",
246 level="info",
247 handler_count=4,
248 using_structlog=_use_structlog,
249 )