所以我有一个像这样的自定义中间件:
其目标是向来自我的 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 来实现的,并且有其自身的问题(与等待内部中间件、流响应和正常响应以及它的调用方式有关)。我还没明白。
以下是您可以做到这一点的方法(受到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!"}
或者,一种可能更好的方法是在中间件函数开始时检查请求的 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"}
另一种解决方案是使用自定义
APIRoute
类,如此处和此处所示,这将允许您将更改应用于response
主体仅您指定的路线 -这将以更简单的方式解决 Swaager UI 的问题。
或者,如果您愿意,您仍然可以使用中间件选项,但您可以将其添加到
子应用程序,而不是将中间件添加到主
app
,如这个答案和这个答案所示—这再次包括仅您需要修改response
的路线,以便在正文中添加一些附加数据。
您将使用从中间件和响应(本例中为 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