FastAPI 全局 HTTP 异常处理器 + 统一响应封装

这是通过全局 HTTP 异常处理器 + 统一响应封装实现的。

应用启动时注册处理器

main.py

configure_api_responses(app)

无效路径触发 404 异常

请求不存在的路径时,FastAPI 底层的 Starlette 路由会抛出 HTTPException,其中:

status_code = 404
detail = "Not Found"

注册的处理器接住这个异常:

@app.exception_handler(HTTPException)
async def http_error(request: Request, exc: HTTPException):
    return error_response(
        exc.status_code, {"detail": exc.detail}, exc.headers
    )

这里导入的是 starlette.exceptions.HTTPException,所以也能捕获框架产生的路由异常。

将异常转换成统一 JSON

[responses.py:22]的 error_response()

  • 通过 ERROR_CODES404 映射为业务码 4004
  • 将字符串 detail 作为 message
  • {"detail": "Not Found"} 放入 data
  • ApiResponse 序列化,返回 JSONResponse

因此响应体是:

{
  "code": 4004,
  "message": "Not Found",
  "data": {"detail": "Not Found"}
}

注意:目前实际返回的 HTTP 状态码是 200,因为 error_response() 中写的是 status_code=200404 被转换成了响应体里的业务码 4004

示例代码

main.py

def create_app(
    settings: Settings | None = None,
    extractor=None,
    rag_answer_service=None,
) -> FastAPI:
    settings = settings or Settings()
    settings.ensure_directories()
    settings.configure_logging(component="api")
    repository = create_document_repository(settings)
    if extractor is None:
        from vipsoft_agent.infrastructure.llm.qwen import QwenRequirementExtractor

        extractor = QwenRequirementExtractor(settings)

     
    app = FastAPI(
        title="VipSoftAgent",
        version="0.1.0",
        description="VipSoftAgent Service",
        lifespan=lifespan,
        redirect_slashes=False,
        docs_url=None,
        redoc_url=None
    )
    configure_api_responses(app) # 配置异常处理器
    .....
    return app


def run() -> None:
    import uvicorn

    uvicorn.run("vipsoft_agent.main:create_app", factory=True, host="0.0.0.0", port=8000)

responses.py


def configure_api_responses(app: FastAPI) -> None:
    @app.exception_handler(HTTPException)
    async def http_error(request: Request, exc: HTTPException):
        return error_response(exc.status_code, {"detail": exc.detail}, exc.headers)

    @app.exception_handler(RequestValidationError)
    async def validation_error(request: Request, exc: RequestValidationError):
        return error_response(422, {"detail": exc.errors()})

    # Keep unexpected errors inside CORS so allowed callers can read the envelope.
    @app.middleware("http")
    async def unexpected_error(request: Request, call_next):
        try:
            return await call_next(request)
        except Exception:
            logger.exception("Unhandled API error method=%s path=%s",
                             request.method, request.url.path)
            return error_response(500, {"detail": "Internal server error"})

    def openapi():
        if app.openapi_schema is None:
            schema = get_openapi(
                title=app.title, version=app.version,
                description=app.description, routes=app.routes,
            )
            for path in schema["paths"].values():
                for operation in path.values():
                    if isinstance(operation, dict) and "responses" in operation:
                        operation["responses"].pop("422", None)
            app.openapi_schema = schema
        return app.openapi_schema

    app.openapi = openapi

def error_response(status: int, data: dict, headers=None) -> JSONResponse:
    detail = data.get("detail")
    if isinstance(detail, str):
        message = detail
    elif isinstance(detail, list) and detail and isinstance(detail[0], dict):
        message = str(detail[0].get("msg", "Request failed"))
    else:
        message = HTTPStatus(status).phrase
    return JSONResponse(
        status_code=200,
        content=ApiResponse(
            code=ERROR_CODES.get(status, status * 10),
            message=message,
            data=jsonable_encoder(data),
        ).model_dump(mode="json"),
        headers=headers,
    )
posted @ 2026-09-10 10:18  VipSoft  阅读(11)  评论(0)    收藏  举报