簡體   English   中英

FastAPI swagger 由於自定義中間件而無法呈現?

[英]FastAPI swagger does not render because of custom Middleware?

所以我有一個這樣的自定義中間件:

它的目標是為我的 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")通過刪除默認參數,swagger 確實成功加載。 但是現在當我點擊任何 API 時,swagger 會告訴我屏幕上的響應負載: Unrecognised response type; displaying content as text Unrecognised response type; displaying content as text

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

克里斯找到了解決部分問題的解決方案,但 swagger 仍未加載。 代碼無限期地掛在中間件級別,頁面仍未加載。

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

這種添加自定義中間件的方式是通過繼承 Starlette 中的 BaseHTTPMiddleware 來工作的,並且有其自身的問題(與在中間件內部等待、流式響應和正常響應以及它的調用方式有關)。 我還不明白。

這是你可以做到的( 受此啟發)。 確保檢查響應的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!"}

更新

或者,一種可能更好的方法是在中間件 function 的開頭檢查請求的 url 路徑(對照您希望將元數據添加到其響應中的路徑/路由的預定義列表),然后相應地繼續。 下面給出示例。 另一種選擇是在 router 中使用 Custom APIRoute class

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

您正在將 swagger html 的主體替換為 json 數據,該數據取自中間件和響應(在本例中為 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內容類型不需要時,才能執行此操作。 (您可以根據需要更改支票)

另一種可能的解決方案

等待下面的 issue 合並到當前的starlette的實現和fastapi開始使用這個版本。

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM