zai / app /core /error_handler.py
bg5's picture
Upload 85 files
e0b68d2 verified
Raw History Blame Contribute Delete
6.79 kB
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""全局错误处理器。
提供统一的异常处理机制:
- AppError 及其子类 → 结构化错误响应
- RequestValidationError → 验证错误响应
- HTTPException → 保持原有行为
- Exception → 兜底处理
所有错误响应遵循 OpenAI API 格式。
"""
import time
from typing import Any, Callable, Dict
from fastapi import Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import HTTPException, RequestValidationError
from fastapi.responses import JSONResponse
from app.exceptions import AppError
from app.utils.logger import logger, log_exception
# -----------------------------------------------------------------------
# OpenAI 兼容错误响应格式
# -----------------------------------------------------------------------
def create_error_response(
message: str,
error_type: str,
code: str,
status_code: int = 500,
details: Dict[str, Any] | None = None,
) -> Dict[str, Any]:
"""创建 OpenAI 兼容的错误响应体。"""
error: Dict[str, Any] = {
"message": message,
"type": error_type,
"code": code,
}
if details:
error["details"] = details
return {"error": error}
# -----------------------------------------------------------------------
# 异常处理器
# -----------------------------------------------------------------------
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
"""处理 AppError 及其子类。
记录错误日志并返回结构化的错误响应。
"""
# 根据状态码选择日志级别
if exc.status_code >= 500:
log_func = logger.error
elif exc.status_code >= 400:
log_func = logger.warning
else:
log_func = logger.info
log_func(
"API error: %s (code=%s, status=%d, path=%s)",
exc.message,
exc.code,
exc.status_code,
request.url.path,
extra={
"error_code": exc.code,
"status_code": exc.status_code,
"path": request.url.path,
"method": request.method,
"details": exc.details,
},
)
return JSONResponse(
status_code=exc.status_code,
content=exc.to_dict(),
)
async def validation_error_handler(
request: Request,
exc: RequestValidationError,
) -> JSONResponse:
"""处理 FastAPI 请求验证错误。"""
errors = exc.errors()
# 提取第一个错误作为主要消息
if errors:
first_error = errors[0]
field = ".".join(str(loc) for loc in first_error.get("loc", []))
message = first_error.get("msg", "Validation failed")
if field:
message = f"{field}: {message}"
else:
message = "Request validation failed"
logger.warning(
"Validation error: %s (path=%s)",
message,
request.url.path,
extra={
"path": request.url.path,
"method": request.method,
"validation_errors": errors,
},
)
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=create_error_response(
message=message,
error_type="ValidationError",
code="validation_error",
status_code=422,
details={"errors": errors},
),
)
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
"""处理 FastAPI HTTPException。
将 HTTPException 转换为 OpenAI 兼容格式。
"""
# 特殊处理重定向
if exc.status_code in (301, 302, 303, 307, 308):
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
headers=exc.headers,
)
logger.warning(
"HTTP exception: %s (status=%d, path=%s)",
exc.detail,
exc.status_code,
request.url.path,
)
return JSONResponse(
status_code=exc.status_code,
content=create_error_response(
message=str(exc.detail),
error_type="HTTPException",
code=f"http_{exc.status_code}",
status_code=exc.status_code,
),
headers=exc.headers,
)
async def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:
"""兜底处理未知异常。
记录完整堆栈,返回通用错误响应。
"""
log_exception(
logger,
"Unhandled exception: %s (path=%s, method=%s)",
type(exc).__name__,
request.url.path,
request.method,
error=exc,
)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=create_error_response(
message="Internal server error",
error_type="InternalServerError",
code="internal_error",
),
)
# -----------------------------------------------------------------------
# 流式响应错误处理
# -----------------------------------------------------------------------
def format_sse_error(
message: str,
code: str = "internal_error",
error_type: str = "InternalError",
) -> str:
"""格式化 SSE 错误响应。
用于流式响应中的错误传递。
Args:
message: 错误消息
code: 错误码
error_type: 错误类型
Returns:
SSE 格式的错误字符串
"""
import json
error_data = {
"error": {
"message": message,
"type": error_type,
"code": code,
}
}
return f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
def format_sse_done() -> str:
"""返回 SSE 结束标记。"""
return "data: [DONE]\n\n"
# -----------------------------------------------------------------------
# 注册函数
# -----------------------------------------------------------------------
def register_exception_handlers(app) -> None:
"""注册所有异常处理器到 FastAPI 应用。
Args:
app: FastAPI 应用实例
"""
from fastapi.exceptions import HTTPException, RequestValidationError
app.add_exception_handler(AppError, app_error_handler)
app.add_exception_handler(RequestValidationError, validation_error_handler)
app.add_exception_handler(HTTPException, http_exception_handler)
app.add_exception_handler(Exception, generic_error_handler)
logger.info("✅ Global exception handlers registered")