| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- """全局异常处理器,统一所有错误响应格式为 {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]
|