"""全局异常处理器。 把各类异常统一转换为 `{code, message, data}` 响应, 避免 FastAPI 默认的 422 `detail` 数组结构泄漏到前端。 注意:参数校验失败按规范映射为 **400**(FastAPI 默认是 422), 因为规范第 9 节只定义了 400/401/403/404/500 五个状态码。 """ import logging from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from starlette.exceptions import HTTPException as StarletteHTTPException from app.core.constants import CODE_BAD_REQUEST, CODE_SERVER_ERROR from app.core.exceptions import AppException from app.core.response import ApiResponse logger = logging.getLogger(__name__) def _build_response(code: int, message: str) -> JSONResponse: """按统一格式构造 JSON 响应。""" body = ApiResponse[None](code=code, message=message, data=None) return JSONResponse(status_code=code, content=body.model_dump()) def _format_validation_errors(exc: RequestValidationError) -> str: """把 pydantic 的校验错误压缩成一行中文提示。 只取首条错误的字段名与说明,避免把完整的错误数组抛给前端。 """ errors = exc.errors() if not errors: return "参数错误" first = errors[0] # loc 形如 ("body", "username"),去掉来源前缀只留字段名 location = ".".join( str(part) for part in first.get("loc", ()) if part not in ("body", "query", "path") ) detail = first.get("msg", "参数错误") return f"参数错误:{location} {detail}" if location else f"参数错误:{detail}" async def handle_app_exception(_: Request, exc: AppException) -> JSONResponse: """处理业务异常,按其 code 返回。""" return _build_response(exc.code, exc.message) async def handle_validation_error(_: Request, exc: RequestValidationError) -> JSONResponse: """处理请求参数校验失败,映射为 400。""" return _build_response(CODE_BAD_REQUEST, _format_validation_errors(exc)) async def handle_http_exception(_: Request, exc: StarletteHTTPException) -> JSONResponse: """处理框架级 HTTP 异常(404 路由不存在、405 方法不允许等)。""" message = exc.detail if isinstance(exc.detail, str) else "请求处理失败" return _build_response(exc.status_code, message) async def handle_unexpected_exception(_: Request, exc: Exception) -> JSONResponse: """兜底处理未预期异常。 对外只返回通用文案,不泄漏堆栈;完整异常记录到日志供排查。 """ logger.exception("未预期的服务端异常") return _build_response(CODE_SERVER_ERROR, "服务端异常,请稍后重试") def register_exception_handlers(app: FastAPI) -> None: """注册全部全局异常处理器。 注册顺序不影响匹配 —— FastAPI 按异常类型精确查找,越具体的类型优先。 """ app.add_exception_handler(AppException, handle_app_exception) # type: ignore[arg-type] app.add_exception_handler(RequestValidationError, handle_validation_error) # type: ignore[arg-type] app.add_exception_handler(StarletteHTTPException, handle_http_exception) # type: ignore[arg-type] app.add_exception_handler(Exception, handle_unexpected_exception)