All checks were successful
continuous-integration/drone/push Build is passing
- 新增告警模块 (alerts): 告警规则配置与触发 - 新增成本管理模块 (cost): 成本统计与分析 - 新增配额模块 (quota): 配额管理与限制 - 新增微信模块 (wechat): 微信相关功能接口 - 新增缓存服务 (cache): Redis 缓存封装 - 新增请求日志中间件 (request_logger) - 新增异常处理和链路追踪中间件 - 更新 dashboard 前端展示 - 更新 SDK stats_client 功能
129 lines
3.9 KiB
Python
129 lines
3.9 KiB
Python
"""
|
|
统一异常处理
|
|
|
|
捕获所有异常,返回统一格式的错误响应,包含 TraceID。
|
|
"""
|
|
import logging
|
|
import traceback
|
|
from typing import Union
|
|
from fastapi import FastAPI, Request, HTTPException
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.exceptions import RequestValidationError
|
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
|
|
from .trace import get_trace_id
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ErrorCode:
|
|
"""错误码常量"""
|
|
BAD_REQUEST = "BAD_REQUEST"
|
|
UNAUTHORIZED = "UNAUTHORIZED"
|
|
FORBIDDEN = "FORBIDDEN"
|
|
NOT_FOUND = "NOT_FOUND"
|
|
VALIDATION_ERROR = "VALIDATION_ERROR"
|
|
RATE_LIMITED = "RATE_LIMITED"
|
|
INTERNAL_ERROR = "INTERNAL_ERROR"
|
|
SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"
|
|
GATEWAY_ERROR = "GATEWAY_ERROR"
|
|
|
|
|
|
STATUS_TO_ERROR_CODE = {
|
|
400: ErrorCode.BAD_REQUEST,
|
|
401: ErrorCode.UNAUTHORIZED,
|
|
403: ErrorCode.FORBIDDEN,
|
|
404: ErrorCode.NOT_FOUND,
|
|
422: ErrorCode.VALIDATION_ERROR,
|
|
429: ErrorCode.RATE_LIMITED,
|
|
500: ErrorCode.INTERNAL_ERROR,
|
|
502: ErrorCode.GATEWAY_ERROR,
|
|
503: ErrorCode.SERVICE_UNAVAILABLE,
|
|
}
|
|
|
|
|
|
def create_error_response(
|
|
status_code: int,
|
|
code: str,
|
|
message: str,
|
|
trace_id: str = None,
|
|
details: dict = None
|
|
) -> JSONResponse:
|
|
"""创建统一格式的错误响应"""
|
|
if trace_id is None:
|
|
trace_id = get_trace_id()
|
|
|
|
error_body = {
|
|
"code": code,
|
|
"message": message,
|
|
"trace_id": trace_id
|
|
}
|
|
|
|
if details:
|
|
error_body["details"] = details
|
|
|
|
return JSONResponse(
|
|
status_code=status_code,
|
|
content={"success": False, "error": error_body},
|
|
headers={"X-Trace-ID": trace_id}
|
|
)
|
|
|
|
|
|
async def http_exception_handler(request: Request, exc: Union[HTTPException, StarletteHTTPException]):
|
|
"""处理 HTTP 异常"""
|
|
trace_id = get_trace_id()
|
|
status_code = exc.status_code
|
|
error_code = STATUS_TO_ERROR_CODE.get(status_code, ErrorCode.INTERNAL_ERROR)
|
|
message = exc.detail if isinstance(exc.detail, str) else str(exc.detail)
|
|
|
|
logger.warning(f"[{trace_id}] HTTP {status_code}: {message}")
|
|
|
|
return create_error_response(
|
|
status_code=status_code,
|
|
code=error_code,
|
|
message=message,
|
|
trace_id=trace_id
|
|
)
|
|
|
|
|
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
"""处理请求验证错误"""
|
|
trace_id = get_trace_id()
|
|
errors = exc.errors()
|
|
error_messages = [f"{'.'.join(str(l) for l in e['loc'])}: {e['msg']}" for e in errors]
|
|
|
|
logger.warning(f"[{trace_id}] 验证错误: {error_messages}")
|
|
|
|
return create_error_response(
|
|
status_code=422,
|
|
code=ErrorCode.VALIDATION_ERROR,
|
|
message="请求参数验证失败",
|
|
trace_id=trace_id,
|
|
details={"validation_errors": error_messages}
|
|
)
|
|
|
|
|
|
async def generic_exception_handler(request: Request, exc: Exception):
|
|
"""处理所有未捕获的异常"""
|
|
trace_id = get_trace_id()
|
|
|
|
logger.error(f"[{trace_id}] 未捕获异常: {type(exc).__name__}: {exc}")
|
|
logger.error(f"[{trace_id}] 堆栈:\n{traceback.format_exc()}")
|
|
|
|
return create_error_response(
|
|
status_code=500,
|
|
code=ErrorCode.INTERNAL_ERROR,
|
|
message="服务器内部错误,请稍后重试",
|
|
trace_id=trace_id
|
|
)
|
|
|
|
|
|
def setup_exception_handlers(app: FastAPI):
|
|
"""配置 FastAPI 应用的异常处理器"""
|
|
app.add_exception_handler(HTTPException, http_exception_handler)
|
|
app.add_exception_handler(StarletteHTTPException, http_exception_handler)
|
|
app.add_exception_handler(RequestValidationError, validation_exception_handler)
|
|
app.add_exception_handler(Exception, generic_exception_handler)
|
|
|
|
logger.info("异常处理器已配置")
|