93 lines
3.2 KiB
Python
93 lines
3.2 KiB
Python
"""业务异常与全局 API 异常处理"""
|
|
|
|
from http import HTTPStatus
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.exceptions import RequestValidationError
|
|
from fastapi.responses import JSONResponse
|
|
from loguru import logger
|
|
from openai import APIError, APITimeoutError
|
|
from starlette.exceptions import HTTPException
|
|
|
|
from app.common.response import error_content
|
|
from app.exceptions import AppError, DecisionError
|
|
|
|
|
|
class BizException(AppError):
|
|
"""可安全返回给调用方的业务错误"""
|
|
|
|
def __init__(
|
|
self,
|
|
code: int,
|
|
msg: str = "请求失败",
|
|
headers: dict[str, str] | None = None,
|
|
) -> None:
|
|
self.code = code
|
|
self.msg = msg
|
|
# 协议响应头,例如 401 时的 WWW-Authenticate
|
|
self.headers = headers
|
|
super().__init__(msg)
|
|
|
|
|
|
async def handle_biz_exception(_request: Request, exc: BizException) -> JSONResponse:
|
|
return JSONResponse(
|
|
status_code=exc.code,
|
|
content=error_content(exc.code, exc.msg),
|
|
headers=exc.headers,
|
|
)
|
|
|
|
|
|
async def handle_validation_exception(
|
|
_request: Request, _exc: RequestValidationError
|
|
) -> JSONResponse:
|
|
# 不返回原始输入,避免校验错误携带用户数据
|
|
return JSONResponse(status_code=422, content=error_content(422, "请求参数校验失败"))
|
|
|
|
|
|
async def handle_http_exception(_request: Request, exc: HTTPException) -> JSONResponse:
|
|
# 保留状态与协议响应头,例如 Allow、WWW-Authenticate 和 Retry-After
|
|
try:
|
|
message = HTTPStatus(exc.status_code).phrase
|
|
except ValueError:
|
|
message = "请求失败"
|
|
# 个别异常自带面向用户的文案,例如请求体超限
|
|
message = getattr(exc, "message", None) or message
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content=error_content(exc.status_code, message),
|
|
headers=exc.headers,
|
|
)
|
|
|
|
|
|
async def handle_unexpected_exception(
|
|
_request: Request, exc: Exception
|
|
) -> JSONResponse:
|
|
# 详细异常仅记录在服务端,不将堆栈或内部错误内容返回给前端
|
|
logger.bind(thread_id=getattr(exc, "_agent_thread_id", "-")).opt(
|
|
exception=exc
|
|
).error("未处理的服务器异常")
|
|
|
|
exc._logged_by_api = True
|
|
|
|
if isinstance(exc, APITimeoutError) or (
|
|
isinstance(exc, DecisionError) and exc.kind == "timeout"
|
|
):
|
|
return JSONResponse(
|
|
status_code=504, content=error_content(504, "模型服务请求超时")
|
|
)
|
|
if isinstance(exc, APIError) or (
|
|
isinstance(exc, DecisionError) and exc.kind != "configuration"
|
|
):
|
|
return JSONResponse(
|
|
status_code=502, content=error_content(502, "模型服务调用失败")
|
|
)
|
|
return JSONResponse(status_code=500, content=error_content(500, "服务内部错误"))
|
|
|
|
|
|
def register_exception_handlers(app: FastAPI) -> None:
|
|
"""集中注册异常处理,接口无需重复捕获"""
|
|
app.add_exception_handler(BizException, handle_biz_exception)
|
|
app.add_exception_handler(RequestValidationError, handle_validation_exception)
|
|
app.add_exception_handler(HTTPException, handle_http_exception)
|
|
app.add_exception_handler(Exception, handle_unexpected_exception)
|