Coverage for src / mcp_server_langgraph / api / error_handlers.py: 77%
31 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-03 00:43 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-03 00:43 +0000
1"""
2FastAPI exception handlers for custom exceptions.
4Provides structured error responses with proper HTTP status codes,
5trace IDs, and user-friendly messages.
7See ADR-0029 for design rationale.
8"""
10import logging
11from typing import Any
13from fastapi import FastAPI, Request
14from fastapi.responses import JSONResponse
16from mcp_server_langgraph.core.exceptions import MCPServerException
18logger = logging.getLogger(__name__)
21def register_exception_handlers(app: FastAPI) -> None:
22 """
23 Register custom exception handlers with FastAPI application.
25 Args:
26 app: FastAPI application instance
28 Usage:
29 from fastapi import FastAPI
30 from mcp_server_langgraph.api.error_handlers import register_exception_handlers
32 app = FastAPI()
33 register_exception_handlers(app)
34 """
36 @app.exception_handler(MCPServerException)
37 async def mcp_exception_handler(request: Request, exc: MCPServerException) -> JSONResponse:
38 """
39 Handle MCP server exceptions.
41 Provides:
42 - Structured JSON error response
43 - Trace ID in response headers
44 - Metrics emission
45 - Detailed logging with context
46 """
47 # Log with full context
48 logger.error(
49 f"Exception: {exc.error_code}",
50 extra={
51 "error_code": exc.error_code,
52 "status_code": exc.status_code,
53 "category": exc.category.value,
54 "retry_policy": exc.retry_policy.value,
55 "trace_id": exc.trace_id,
56 "metadata": exc.metadata,
57 "user_message": exc.user_message,
58 "request_method": request.method,
59 "request_url": str(request.url),
60 },
61 )
63 # Record metric
64 try:
65 from mcp_server_langgraph.observability.telemetry import error_counter
67 error_counter.add(
68 1,
69 attributes={
70 "error_code": exc.error_code,
71 "status_code": str(exc.status_code),
72 "category": exc.category.value,
73 "retry_policy": exc.retry_policy.value,
74 },
75 )
76 except Exception as metric_error:
77 logger.warning(f"Failed to record error metric: {metric_error}")
79 # Build response headers
80 headers = {}
81 if exc.trace_id: 81 ↛ 82line 81 didn't jump to line 82 because the condition on line 81 was never true
82 headers["X-Trace-ID"] = exc.trace_id
84 # Add Retry-After header for rate limit errors
85 if exc.status_code == 429:
86 retry_after = exc.metadata.get("retry_after", 60)
87 headers["Retry-After"] = str(retry_after)
89 # Return JSON response
90 return JSONResponse(
91 status_code=exc.status_code,
92 content=exc.to_dict(),
93 headers=headers,
94 )
96 @app.exception_handler(Exception)
97 async def generic_exception_handler(request: Request, exc: Exception) -> JSONResponse:
98 """
99 Handle unexpected exceptions.
101 Wraps generic exceptions in InternalServerError for consistent handling.
102 """
103 # Log unexpected error
104 logger.error(
105 f"Unexpected exception: {type(exc).__name__}",
106 extra={
107 "exception_type": type(exc).__name__,
108 "request_method": request.method,
109 "request_url": str(request.url),
110 },
111 )
113 # Wrap in InternalServerError
114 from mcp_server_langgraph.core.exceptions import UnexpectedError
116 wrapped_exc = UnexpectedError(
117 message=str(exc),
118 metadata={
119 "original_exception": type(exc).__name__,
120 "request_method": request.method,
121 "request_path": str(request.url.path),
122 },
123 cause=exc,
124 )
126 return await mcp_exception_handler(request, wrapped_exc)
128 logger.info("Exception handlers registered")
131def create_error_response(
132 error_code: str,
133 message: str,
134 status_code: int = 500,
135 metadata: dict[str, Any] | None = None,
136 trace_id: str | None = None,
137) -> dict[str, Any]:
138 """
139 Create a standardized error response dictionary.
141 Args:
142 error_code: Machine-readable error code
143 message: Human-readable error message
144 status_code: HTTP status code
145 metadata: Additional error context
146 trace_id: OpenTelemetry trace ID
148 Returns:
149 Standardized error response dictionary
151 Usage:
152 return create_error_response(
153 error_code="auth.token_expired",
154 message="Your session has expired",
155 status_code=401,
156 metadata={"user_id": "user_123"},
157 trace_id="abc123def456"
158 )
159 """
160 return {
161 "error": {
162 "code": error_code,
163 "message": message,
164 "status_code": status_code,
165 "metadata": metadata or {},
166 "trace_id": trace_id,
167 }
168 }