So I have a custom middleware like this:
Its objective is to add some meta_data fields to every response from all endpoints of my FastAPI app.
@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 )
However, when I served my app using uvicorn, and launched the swagger URL, here is what I see:
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)
With a lot of debugging, I found that this error was due to the custom middleware and specifically this line:
body = json.dumps(data, indent=2, default=str).encode("utf-8")
If I simply comment out this line, swagger renders just fine for me. However, I need this line for passing the content argument in Response from Middleware. How to sort this out?
UPDATE:
I tried the following:
body = json.dumps(data, indent=2).encode("utf-8")
by removing default arg, the swagger did successfully load. But now when I hit any of the APIs, here is what swagger tells me along with response payload on screen:
Unrecognised response type; displaying content as text
More Updates (6th April 2022):
Got a solution to fix 1 part of the problem by Chris, but the swagger wasn’t still loading. The code was hung up in the middleware level indefinitely and the page was not still loading.
So, I found in all these places:
- https://github.com/encode/starlette/issues/919
- Blocked code while using middleware and dependency injections to log requests in FastAPI(Python)
- https://github.com/tiangolo/fastapi/issues/394
that this way of adding custom middleware works by inheriting from BaseHTTPMiddleware in Starlette and has its own issues (something to do with awaiting inside middleware, streamingresponse and normal response, and the way it is called). I don’t understand it yet.
Advertisement
Answer
Here’s how you could do that (inspired by this). Make sure to check the Content-Type
of the response (as shown below), so that you can modify it by adding the metadata
, only if it is of application/json
type.
For the OpenAPI (Swagger UI) to render (both /docs
and /redoc
), make sure to check whether openapi
key is not present in the response, so that you can proceed modifying the response only in that case. If you happen to have a key with such a name in your response data, then you could have additional checks using further keys that are present in the response for the OpenAPI, e.g., info
, version
, paths
, and, if needed, you can check against their values too.
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!"}
Update 1
Alternatively, a likely better approach would be to check for the request’s url path at the start of the middleware function (against a pre-defined list of paths/routes that you would like to add metadata to their responses), and proceed accordingly. Example is given below.
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"}
Update 2
Another solution would be to use a custom APIRoute
class, as demonstrated here and here, which would allow you to apply the changes on the response
body only to routes that you specify—which would solve the issue with Swaager UI in a more easy way.
You could still use the middleware option if you wish, but instead of adding the middleware to the main app
, you could add it to a sub application—as shown in this answer—that includes again only the routes for which you need to modify the response
in order to add some additional data in the body.