Coverage for netrun_errors \ handlers.py: 76%
42 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-25 19:24 -0800
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-25 19:24 -0800
1"""
2Global exception handlers for FastAPI applications.
4Provides centralized exception handling with structured JSON responses,
5correlation ID injection, and logging integration.
6"""
8import logging
9from typing import Union
11from fastapi import FastAPI, Request, status
12from fastapi.exceptions import RequestValidationError
13from fastapi.responses import JSONResponse
14from starlette.exceptions import HTTPException as StarletteHTTPException
16from .base import NetrunException
18logger = logging.getLogger(__name__)
21async def netrun_exception_handler(
22 request: Request, exc: NetrunException
23) -> JSONResponse:
24 """
25 Handle NetrunException instances with structured JSON responses.
27 Args:
28 request: FastAPI request object
29 exc: NetrunException instance
31 Returns:
32 JSONResponse with structured error format
33 """
34 # Add request path to error details
35 error_dict = exc.to_dict()
36 error_dict["error"]["path"] = str(request.url.path)
38 # Log error with correlation ID
39 logger.error(
40 f"NetrunException: {exc.error_code} - {exc.message}",
41 extra={
42 "correlation_id": exc.correlation_id,
43 "error_code": exc.error_code,
44 "path": request.url.path,
45 "method": request.method,
46 },
47 )
49 return JSONResponse(
50 status_code=exc.status_code,
51 content=error_dict,
52 )
55async def validation_exception_handler(
56 request: Request, exc: RequestValidationError
57) -> JSONResponse:
58 """
59 Handle FastAPI request validation errors with structured format.
61 Args:
62 request: FastAPI request object
63 exc: RequestValidationError instance
65 Returns:
66 JSONResponse with validation error details
67 """
68 from datetime import datetime, timezone
70 correlation_id = NetrunException._generate_correlation_id()
72 error_response = {
73 "error": {
74 "code": "VALIDATION_ERROR",
75 "message": "Request validation failed",
76 "details": {
77 "validation_errors": exc.errors(),
78 },
79 "correlation_id": correlation_id,
80 "timestamp": datetime.now(timezone.utc).isoformat(),
81 "path": str(request.url.path),
82 }
83 }
85 logger.warning(
86 f"Validation error: {request.method} {request.url.path}",
87 extra={
88 "correlation_id": correlation_id,
89 "validation_errors": exc.errors(),
90 },
91 )
93 return JSONResponse(
94 status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
95 content=error_response,
96 )
99async def http_exception_handler(
100 request: Request, exc: Union[StarletteHTTPException, Exception]
101) -> JSONResponse:
102 """
103 Handle generic HTTP exceptions with structured format.
105 Args:
106 request: FastAPI request object
107 exc: HTTPException or generic Exception
109 Returns:
110 JSONResponse with structured error format
111 """
112 from datetime import datetime, timezone
114 correlation_id = NetrunException._generate_correlation_id()
116 # Determine status code
117 if isinstance(exc, StarletteHTTPException):
118 status_code = exc.status_code
119 message = exc.detail if isinstance(exc.detail, str) else str(exc.detail)
120 else:
121 status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
122 message = "An unexpected error occurred"
124 error_response = {
125 "error": {
126 "code": "HTTP_ERROR",
127 "message": message,
128 "details": {},
129 "correlation_id": correlation_id,
130 "timestamp": datetime.now(timezone.utc).isoformat(),
131 "path": str(request.url.path),
132 }
133 }
135 logger.error(
136 f"HTTP exception: {status_code} - {message}",
137 extra={
138 "correlation_id": correlation_id,
139 "status_code": status_code,
140 "path": request.url.path,
141 "method": request.method,
142 },
143 exc_info=True if status_code >= 500 else False,
144 )
146 return JSONResponse(
147 status_code=status_code,
148 content=error_response,
149 )
152async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
153 """
154 Handle unhandled exceptions with structured format.
156 Args:
157 request: FastAPI request object
158 exc: Exception instance
160 Returns:
161 JSONResponse with generic error format
162 """
163 from datetime import datetime, timezone
165 correlation_id = NetrunException._generate_correlation_id()
167 error_response = {
168 "error": {
169 "code": "INTERNAL_SERVER_ERROR",
170 "message": "An unexpected error occurred. Please try again later.",
171 "details": {},
172 "correlation_id": correlation_id,
173 "timestamp": datetime.now(timezone.utc).isoformat(),
174 "path": str(request.url.path),
175 }
176 }
178 logger.exception(
179 f"Unhandled exception: {type(exc).__name__}",
180 extra={
181 "correlation_id": correlation_id,
182 "path": request.url.path,
183 "method": request.method,
184 "exception_type": type(exc).__name__,
185 },
186 )
188 return JSONResponse(
189 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
190 content=error_response,
191 )
194def install_exception_handlers(app: FastAPI) -> None:
195 """
196 Install all Netrun exception handlers on a FastAPI application.
198 Usage:
199 from fastapi import FastAPI
200 from netrun_errors import install_exception_handlers
202 app = FastAPI()
203 install_exception_handlers(app)
205 Args:
206 app: FastAPI application instance
207 """
208 app.add_exception_handler(NetrunException, netrun_exception_handler)
209 app.add_exception_handler(RequestValidationError, validation_exception_handler)
210 app.add_exception_handler(StarletteHTTPException, http_exception_handler)
211 app.add_exception_handler(Exception, unhandled_exception_handler)
213 logger.info("Netrun exception handlers installed successfully")