【问题标题】:FastAPI swagger does not render because of custom Middleware?由于自定义中间件,FastAPI swagger 不渲染?
【发布时间】:2022-08-24 13:26:21
【问题描述】:

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

它的目标是为我的 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 为我的应用程序提供服务并启动了招摇网址时,我看到了以下内容:


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 日):

克里斯找到了解决部分问题的解决方案,但大摇大摆的仍然没有加载。代码无限期地挂在中间件级别,页面仍未加载。

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

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

    标签: python fastapi


    【解决方案1】:

    这是你可以做到的(灵感来自this)。一定要检查响应的Content-Type(如下图),这样你就可以通过添加metadata来修改它,只有它是application/json类型。

    更新 1

    对于要呈现的 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("/")
    async def foo(request: Request):
            return {"hello": "world!"}
    

    更新 2

    或者,一种可能更好的方法是在中间件函数的开头检查请求的 url 路径(根据您希望将元数据添加到其响应中的路径/路由的预定义列表),并相应地继续。或者,您可以使用Custom APIRoute class in a router

    routes_with_middleware = ["/"]
    
    @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:
            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
    

    工作示例

    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("/")
    async def root():
            return {"hello": "world!"}
    
    @app.get("/items/{id}")
    async def get_item(id: int):
            return {"Item": id}
    
    @app.get("/courses/{code}")
    async def get_course(code: my_constr):
            return {"course_code": code, "course_title": "Deep Learning"}
    

    【讨论】:

    • 嘿@Chris,我试过你的解决方案。 (假设您的意思是content_type = response.headers.get('Content-Type' )但是,我遇到了同样的错误。我改变了我使用body = json.dumps(data, indent=2, default=str).encode("utf-8") 的方式,但它提出了一个新问题(有问题的更新),但仍然没有解决我的问题。
    • 嘿@Chris,我尝试了这个解决方案,但是代码流进入了一个永久循环,中间件就被挂断了。 Swagger 没有加载,任何其他请求都没有工作。经过进一步检查,我发现不鼓励这种创建中间件的方法。添加有问题的相关链接,因为 cmets 有限制。 (但是,由于完全其他原因,我的用例仍未解决,因此在 SO 中提出了不同的问题)
    • 好吧,给定的代码不起作用,招摇仍然没有加载。是的,正如我在评论中提到的,对于我正在尝试的新技术,我已经打开了另一个 SO 问题来涵盖这个"However my use-case is still unsolved due to completely other reasons..."
    • 仅使用此中间件尝试了我的应用程序,并且成功了。我的坏,应该更清楚。可能由 elastic-apm 等提供的其他官方中间件导致挂起问题。将在其他 SO 问题中探索更多内容,谢谢@Chris!
    【解决方案2】:

    您正在用取自中间件和响应(在本例中为 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 响应都需要添加metadatahtml 内容类型不需要的情况下才能完成。 (您可以根据需要更改支票)

    另一种可能的解决方案

    等待以下问题合并到当前的starlettes 实现和fastapi 开始使用此版本。

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

    【讨论】:

    • 嘿@Isabi,我在响应类型是JSON而不是HTML的情况下面临这个错误,所以我的data键的值是JSON对象上的字节字符串。
    • @raghavsikaria 你试过 response.json() 吗?
    猜你喜欢
    • 1970-01-01
    • 2015-03-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-29
    • 2022-01-10
    • 2013-07-27
    • 1970-01-01
    相关资源
    最近更新 更多