FastAPI:由于自定义中间件,Swagger UI 无法呈现

问题描述 投票:0回答:2

所以我有一个像这样的自定义中间件:

其目标是向来自我的 FastAPI 应用程序的所有端点的每个响应添加一些元数据字段。


@app.middelware("http")
async def add_metadata_to_response_payload(request: Request, call_next):

    response = await call_next(request)

    body = b""
    async for chunk in response.body_iterator:
        body+=chunk


    data = {}
    data["data"] = json.loads(body.decode())
    data["metadata"] = {
        "some_data_key_1": "some_data_value_1",
        "some_data_key_2": "some_data_value_2",
        "some_data_key_3": "some_data_value_3"
    }

    body = json.dumps(data, indent=2, default=str).encode("utf-8")

    return Response(
        content=body,
        status_code=response.status_code,
        media_type=response.media_type
    )

但是,当我使用 uvicorn 为我的应用程序提供服务并启动 swagger URL 时,我看到的是:


Unable to render this definition

The provided definition does not specify a valid version field.

Please indicate a valid Swagger or OpenAPI version field. Supported version fields are
Swagger: "2.0" and those that match openapi: 3.0.n (for example, openapi: 3.0.0)

经过大量调试,我发现这个错误是由于自定义中间件造成的,特别是这一行:

body = json.dumps(data, indent=2, default=str).encode("utf-8")

如果我简单地注释掉这一行,swagger 渲染对我来说就很好了。但是,我需要这一行来传递来自中间件的响应中的内容参数。怎么解决这个问题?

更新

我尝试了以下方法:

body = json.dumps(data, indent=2).encode("utf-8")
通过删除默认参数,swagger 已成功加载。但现在,当我点击任何 API 时,swagger 会告诉我以下内容以及屏幕上的响应负载:
Unrecognised response type; displaying content as text

更多更新(2022 年 4 月 6 日):

Chris 找到了解决部分问题的解决方案,但 swagger 尚未加载。代码无限期地挂在中间件级别,并且页面尚未加载。

所以,我在所有这些地方都找到了:

这种添加自定义中间件的方式是通过继承 Starlette 中的 BaseHTTPMiddleware 来实现的,并且有其自身的问题(与等待内部中间件、流响应和正常响应以及它的调用方式有关)。我还没明白。

python swagger fastapi swagger-ui openapi
2个回答
5
投票

以下是您可以做到这一点的方法(受到this的启发)。请务必检查响应的

Content-Type
(如下所示),以便您可以通过添加
metadata
来修改它,前提是它是
application/json
类型。

要呈现 OpenAPI (Swagger UI)(

/docs
/redoc
),请务必检查响应中是否不存在
openapi
键,以便您可以仅在这种情况下继续修改响应。如果您的响应数据中碰巧有一个具有此类名称的密钥,那么您可以使用 OpenAPI 响应中存在的其他密钥进行额外检查,例如
info
version
paths
和,如果需要,您也可以检查它们的值。

from fastapi import FastAPI, Request, Response
import json

app = FastAPI()

@app.middleware("http")
async def add_metadata_to_response_payload(request: Request, call_next):
    response = await call_next(request)
    content_type = response.headers.get('Content-Type')
    if content_type == "application/json":
        response_body = [section async for section in response.body_iterator]
        resp_str = response_body[0].decode()  # converts "response_body" bytes into string
        resp_dict = json.loads(resp_str)  # converts resp_str into dict 
        #print(resp_dict)
        if "openapi" not in resp_dict:
            data = {}
            data["data"] = resp_dict  # adds the "resp_dict" to the "data" dictionary
            data["metadata"] = {
                "some_data_key_1": "some_data_value_1",
                "some_data_key_2": "some_data_value_2",
                "some_data_key_3": "some_data_value_3"}
            resp_str = json.dumps(data, indent=2)  # converts dict into JSON string
        
        return Response(content=resp_str, status_code=response.status_code, media_type=response.media_type)
        
    return response


@app.get("/")
def foo(request: Request):
    return {"hello": "world!"}

更新1

或者,一种可能更好的方法是在中间件函数开始时检查请求的 url 路径(对照您想要将元数据添加到其响应中的预定义路径/路由列表),然后进行相应操作。下面给出了示例。

from fastapi import FastAPI, Request, Response, Query
from pydantic import constr
from fastapi.responses import JSONResponse
import re
import uvicorn
import json

app = FastAPI()
routes_with_middleware = ["/"]
rx = re.compile(r'^(/items/\d+|/courses/[a-zA-Z0-9]+)$')  # support routes with path parameters
my_constr = constr(regex="^[a-zA-Z0-9]+$")

@app.middleware("http")
async def add_metadata_to_response_payload(request: Request, call_next):
    response = await call_next(request)
    if request.url.path not in routes_with_middleware and not rx.match(request.url.path):
        return response
    else:
        content_type = response.headers.get('Content-Type')
        if content_type == "application/json":
            response_body = [section async for section in response.body_iterator]
            resp_str = response_body[0].decode()  # converts "response_body" bytes into string
            resp_dict = json.loads(resp_str)  # converts resp_str into dict 
            data = {}
            data["data"] = resp_dict  # adds "resp_dict" to the "data" dictionary
            data["metadata"] = {
                "some_data_key_1": "some_data_value_1",
                "some_data_key_2": "some_data_value_2",
                "some_data_key_3": "some_data_value_3"}
            resp_str = json.dumps(data, indent=2)  # converts dict into JSON string
            return Response(content=resp_str, status_code=response.status_code, media_type="application/json")


@app.get("/")
def root():
    return {"hello": "world!"}

@app.get("/items/{id}")
def get_item(id: int):
    return {"Item": id}

@app.get("/courses/{code}")
def get_course(code: my_constr):
    return {"course_code": code, "course_title": "Deep Learning"}

更新2

另一种解决方案是使用自定义

APIRoute
,如此处此处所示,这将允许您将更改应用于
response
主体您指定的路线 -这将以更简单的方式解决 Swaager UI 的问题。

或者,如果您愿意,您仍然可以使用中间件选项,但您可以将其添加到

子应用程序
,而不是将中间件添加到主app,如这个答案这个答案所示—这再次包括您需要修改
response
的路线,以便在正文中添加一些附加数据。


0
投票

您将使用从中间件和响应(本例中为 html 响应)获取的 json 数据替换 swagger html 的正文。

你最终会得到类似的东西

{
    "data": "<html>....</html>",
    "metadata": {
        "some_data_key_1": "some_data_value_1",
        "some_data_key_2": "some_data_value_2",
        "some_data_key_3": "some_data_value_3"
    }
}

当然这是行不通的。

可能的解决方案

检查中间件中响应的内容类型。如果是,请延长响应时间

json
,否则保持原样。

注意: 仅当可以安全地假设每个

json
响应都需要添加
metadata
html
内容类型不需要时,才能完成此操作。 (您可以根据需要更改支票)

另一种可能的解决方案

等待以下问题合并到当前

starlette
的实现中并
fastapi
开始使用此版本。

https://github.com/tiangolo/fastapi/issues/1174 https://github.com/encode/starlette/pull/1286

© www.soinside.com 2019 - 2024. All rights reserved.