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

4sup72z8  于 2023-03-18  发布在  其他
关注(0)|答案(2)|浏览(384)

我有一个定制的中间件,如下所示:
它的目标是向来自FastAPI应用程序所有端点的每个响应添加一些 meta_data字段。

@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")通过删除默认的arg,swagger确实成功加载了。但是现在当我点击任何一个API时,swagger会告诉我以下内容沿着屏幕上的响应负载:Unrecognised response type; displaying content as text
更多更新(2022年4月6日):
Chris提供了一个解决方案来修复问题的一部分,但是swagger仍然无法加载,代码被无限期地挂在中间件层,页面也无法加载。
所以,我在所有这些地方发现:

这种添加自定义中间件的方式是继承Starlette的BaseHTTPMiddleware,有自己的问题(中间件内部等待、streamingresponse和normal response、调用方式等问题),我还不明白。

4dbbbstv

4dbbbstv1#

下面是您如何做到这一点(受此启发):确保检查响应的Content-Type(如下所示),以便您可以通过添加metadata来修改它,只有当它是application/json类型时。
对于OpenAPI( Swagger UI)渲染(/docs/redoc),请确保检查openapi密钥是否不存在于响应中,以便只有在这种情况下才能继续修改响应。如果您的响应数据中碰巧有这样一个名称的密钥,则可以使用OpenAPI响应中存在的其他密钥进行额外检查,例如infoversionpaths,如果需要,您也可以检查它们的值。

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")

    return response

@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类,如herehere所示,这将允许您将response主体上的更改应用于您指定的路由-这将以更简单的方式解决Swaager UI的问题。
如果愿意,您仍然可以使用中间件选项,但不是将中间件添加到主app中,而是将其添加到子应用程序中(如this answerthis answer所示),该子应用程序再次包含您需要修改response以便在主体中添加一些额外数据的路由。

pkbketx9

pkbketx92#

您将用取自中间件和响应(本例中为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/1174https://github.com/encode/starlette/pull/1286

相关问题