FastAPI middleware
NOTE: Python
3.11.9
and FastAPI0.111.0
is used throughout this article.
Creating middleware within FastAPI framework is really simple: you just create a regular handler with extra call_next
parameter and decorate it with @app.middleware("http")
.
1 | import time |
Then you make sure to invoke rest of the processing chain by calling await call_next(request)
and return response
and the very end.
On the other hand, when it comes to something more complex like transforming existing body or modifying immutable headers, it’s not that straightforward to do.
Usage of such a middleware is still simple, so in case you want to add automatic redirect to https endpoints you can use provided HTTPSRedirectMiddleware one:
1 | app = FastAPI() |
But writing your own sophisticated middleware is a bit trickier.
Modifying request body
Let’s imagine that we want to strip all the whitespace characters within the payload body we send to our server. First we would create a simple helper function which recursively modifies input data in place.
1 | def strip_whitespace(data: Any) -> Any: |
Just to be clear: you should use Pydantic models in order to coerce/validate the data you pass to the handlers, so this is just an illustrative example here.
To define middleware you need to create a class that accepts ASGI app and make it callable by implementing async def __call__(self, scope: Scope, receive: Receive, send: Send)
method
1 | from starlette.types import ASGIApp, Message, Receive, Scope, Send |
Note that we are also using Starlette types here as FastAPI itself is based on the Starlette framework and heavily use its toolkit.
Main part here is update_body
closure (just to simplify access to receive
function, but it can be a method or a separate function as well).
1 | body: bytes = message["body"].decode() |
We load json payload from our request first
1 | json_stripped = strip_whitespace(json_body) |
and then transform it using previously implemented helper function. All of the downstreamed middleware alongside with route handlers will receive request containing our adjusted payload.
Modifying response body
Now let’s check even more complex middleware where we need to both modify response body and adjust response headers accordingly to the changes made.
We start by defining a similar middleware class which accepts app and has __call__
method on it
1 | class ListWrapMiddleware: |
Here instead of having all the logic defined within middleware class we offload processing to a custom responder
1 | from starlette.datastructures import MutableHeaders |
First, we preserve the initial message and wait for the response.body
message to come. Then we load json body the same way we did before and do any tranformation required. In the example above we wrap response into the nested stucture that count total elements in case of an array and add total
property to it. The list itself is returned under the items
property.
After that, we create headers structure that is allowed to be mutated and change Content-Length header to reflect that our data have been updated.
Finally, we send initial saved response.start
part and our modified body back to the client (or to the any other middleware down the line).
To make sure that transformation are a part of request/response cycle we need to install it on the app.
1 | app.add_middleware(WhitespaceStripMiddleware) |
Running webserver
For the validation we are going to add a simple handler that echoes back the json payload being sent
1 |
|
Then, assuming you have just single main.py
file for the webserver code you can run it as
1 | fastapi dev |
Triggering the request using curl should pass the data though our middleware and return modified structure with all the string values stripped.
1 | curl -X POST -H "Content-Type: application/json" \ |
Response in your terminal for the request above should look like this
1 | { "total": 3, "items": ["one", "two", { "nested": "three" }] } |
As you can see we received data back wrapped into object with extra information and all of the values were subjects of leading/trailing whitespace removal.
At this point you should be able to implement any of the middleware logic in your app using the approach described. But before trying to invent anything, check this repository which contains a lot of useful middleware, so you can cover most of the daily web-realted use cases.
Have fun, see you in the next one.