exception_handlers.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. """全局异常处理器,统一所有错误响应格式为 {code, data, msg}。"""
  2. from __future__ import annotations
  3. import logging
  4. from uuid import uuid4
  5. from fastapi import FastAPI, Request, status
  6. from fastapi.encoders import jsonable_encoder
  7. from fastapi.exceptions import RequestValidationError
  8. from starlette.exceptions import HTTPException as StarletteHTTPException
  9. from starlette.responses import JSONResponse
  10. from app.core.response import BusinessError, ResponseCode, error_response
  11. logger = logging.getLogger(__name__)
  12. def _get_request_info(request: Request) -> str:
  13. """提取请求信息用于日志记录(不记录 query string 避免敏感信息泄露)。"""
  14. x_forwarded_for = request.headers.get("x-forwarded-for", "")
  15. real_ip = x_forwarded_for.split(",")[0].strip() if x_forwarded_for else ""
  16. client_host = real_ip or (request.client.host if request.client else "unknown")
  17. return f"{request.method} {request.url.path} (client: {client_host})"
  18. # HTTP 状态码 -> 业务错误码 的映射
  19. _HTTP_TO_CODE: dict[int, ResponseCode] = {
  20. 400: ResponseCode.REQUEST_INVALID,
  21. 404: ResponseCode.NOT_FOUND,
  22. 405: ResponseCode.METHOD_NOT_ALLOWED,
  23. 422: ResponseCode.PARAM_INVALID,
  24. 502: ResponseCode.UPSTREAM_ERROR,
  25. 504: ResponseCode.UPSTREAM_TIMEOUT,
  26. }
  27. async def validation_exception_handler(
  28. request: Request, exc: RequestValidationError
  29. ) -> JSONResponse:
  30. """处理 Pydantic 请求验证错误(如 page=-1、缺失必填参数等)。
  31. 返回示例:
  32. {
  33. "code": 1001,
  34. "data": [{"loc": [...], "msg": "...", "type": "..."}],
  35. "msg": "参数验证失败"
  36. }
  37. """
  38. request_info = _get_request_info(request)
  39. logger.warning("参数验证失败 [%s]: %s", request_info, exc.errors())
  40. return error_response(
  41. code=ResponseCode.PARAM_INVALID,
  42. data=jsonable_encoder(exc.errors()),
  43. http_status=status.HTTP_422_UNPROCESSABLE_ENTITY,
  44. )
  45. async def http_exception_handler(
  46. request: Request, exc: StarletteHTTPException
  47. ) -> JSONResponse:
  48. """处理 Starlette/FastAPI HTTPException(404/405/502 等)。"""
  49. request_info = _get_request_info(request)
  50. status_code = exc.status_code
  51. if status_code >= 500:
  52. logger.error("服务器错误 [%s]: %s", request_info, exc.detail)
  53. elif status_code >= 400:
  54. logger.warning("客户端错误 [%s]: %s", request_info, exc.detail)
  55. code = _HTTP_TO_CODE.get(status_code, ResponseCode.INTERNAL_ERROR)
  56. return error_response(
  57. code=code,
  58. msg=str(exc.detail) if exc.detail else None,
  59. http_status=status_code,
  60. headers=exc.headers,
  61. )
  62. async def business_exception_handler(
  63. request: Request, exc: BusinessError
  64. ) -> JSONResponse:
  65. """处理业务异常(BusinessError)。"""
  66. request_info = _get_request_info(request)
  67. logger.warning(
  68. "业务异常 [%s]: code=%s msg=%s", request_info, exc.code, exc.msg
  69. )
  70. return error_response(
  71. code=exc.response_code,
  72. msg=exc.msg,
  73. data=exc.data,
  74. http_status=exc.http_status,
  75. )
  76. async def general_exception_handler(
  77. request: Request, exc: Exception
  78. ) -> JSONResponse:
  79. """兜底异常处理器,捕获所有未处理的异常,避免堆栈信息泄露。
  80. 注意:仅捕获 Exception,不捕获 BaseException(如 asyncio.CancelledError),
  81. 以确保 FastAPI lifespan 的取消逻辑正常工作。严禁改为捕获 BaseException!
  82. """
  83. request_info = _get_request_info(request)
  84. request_id = str(uuid4())
  85. logger.exception("未捕获异常 [%s] [request_id=%s]", request_info, request_id)
  86. return error_response(
  87. code=ResponseCode.INTERNAL_ERROR,
  88. data={"request_id": request_id},
  89. http_status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  90. )
  91. def register_exception_handlers(app: FastAPI) -> None:
  92. """向 FastAPI 应用注册所有全局异常处理器。"""
  93. app.add_exception_handler(RequestValidationError, validation_exception_handler)
  94. app.add_exception_handler(StarletteHTTPException, http_exception_handler)
  95. app.add_exception_handler(BusinessError, business_exception_handler) # type: ignore[arg-type]
  96. app.add_exception_handler(Exception, general_exception_handler) # type: ignore[arg-type]