"""业务异常与全局 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 = "请求失败" 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)