[英]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 響應中存在的其他密鑰進行額外檢查,例如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!"}
或者,一種可能更好的方法是在中間件 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.