"""全局异常处理器,统一所有错误响应格式为 {code, data, msg}。""" from __future__ import annotations import logging from uuid import uuid4 from fastapi import FastAPI, Request, status from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.responses import JSONResponse from app.core.response import BusinessError, ResponseCode, error_response logger = logging.getLogger(__name__) def _get_request_info(request: Request) -> str: """提取请求信息用于日志记录(不记录 query string 避免敏感信息泄露)。""" x_forwarded_for = request.headers.get("x-forwarded-for", "") real_ip = x_forwarded_for.split(",")[0].strip() if x_forwarded_for else "" client_host = real_ip or (request.client.host if request.client else "unknown") return f"{request.method} {request.url.path} (client: {client_host})" # HTTP 状态码 -> 业务错误码 的映射 _HTTP_TO_CODE: dict[int, ResponseCode] = { 400: ResponseCode.REQUEST_INVALID, 404: ResponseCode.NOT_FOUND, 405: ResponseCode.METHOD_NOT_ALLOWED, 422: ResponseCode.PARAM_INVALID, 502: ResponseCode.UPSTREAM_ERROR, 504: ResponseCode.UPSTREAM_TIMEOUT, } async def validation_exception_handler( request: Request, exc: RequestValidationError ) -> JSONResponse: """处理 Pydantic 请求验证错误(如 page=-1、缺失必填参数等)。 返回示例: { "code": 1001, "data": [{"loc": [...], "msg": "...", "type": "..."}], "msg": "参数验证失败" } """ request_info = _get_request_info(request) logger.warning("参数验证失败 [%s]: %s", request_info, exc.errors()) return error_response( code=ResponseCode.PARAM_INVALID, data=jsonable_encoder(exc.errors()), http_status=status.HTTP_422_UNPROCESSABLE_ENTITY, ) async def http_exception_handler( request: Request, exc: StarletteHTTPException ) -> JSONResponse: """处理 Starlette/FastAPI HTTPException(404/405/502 等)。""" request_info = _get_request_info(request) status_code = exc.status_code if status_code >= 500: logger.error("服务器错误 [%s]: %s", request_info, exc.detail) elif status_code >= 400: logger.warning("客户端错误 [%s]: %s", request_info, exc.detail) code = _HTTP_TO_CODE.get(status_code, ResponseCode.INTERNAL_ERROR) return error_response( code=code, msg=str(exc.detail) if exc.detail else None, http_status=status_code, headers=exc.headers, ) async def business_exception_handler( request: Request, exc: BusinessError ) -> JSONResponse: """处理业务异常(BusinessError)。""" request_info = _get_request_info(request) logger.warning( "业务异常 [%s]: code=%s msg=%s", request_info, exc.code, exc.msg ) return error_response( code=exc.response_code, msg=exc.msg, data=exc.data, http_status=exc.http_status, ) async def general_exception_handler( request: Request, exc: Exception ) -> JSONResponse: """兜底异常处理器,捕获所有未处理的异常,避免堆栈信息泄露。 注意:仅捕获 Exception,不捕获 BaseException(如 asyncio.CancelledError), 以确保 FastAPI lifespan 的取消逻辑正常工作。严禁改为捕获 BaseException! """ request_info = _get_request_info(request) request_id = str(uuid4()) logger.exception("未捕获异常 [%s] [request_id=%s]", request_info, request_id) return error_response( code=ResponseCode.INTERNAL_ERROR, data={"request_id": request_id}, http_status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) def register_exception_handlers(app: FastAPI) -> None: """向 FastAPI 应用注册所有全局异常处理器。""" app.add_exception_handler(RequestValidationError, validation_exception_handler) app.add_exception_handler(StarletteHTTPException, http_exception_handler) app.add_exception_handler(BusinessError, business_exception_handler) # type: ignore[arg-type] app.add_exception_handler(Exception, general_exception_handler) # type: ignore[arg-type]