20

I have read FastAPI's documentation about middlewares (specifically, the middleware tutorial, the CORS middleware section and the advanced middleware guide), but couldn't find a concrete example of how to write a middleware class which you can add using the add_middleware function (in contrast to a basic middleware function added using a decorator) there nor on this site.

The reason I prefer to use add_middleware over the app based decorator, is that I want to write a middleware in a shared library that will be used by several different projects, and therefore I can't tie it to a specific FastAPI instance.

So my question is: how do you do it?

Dean Gurvitz
  • 854
  • 1
  • 10
  • 24
  • The specification is the general ASGI middleware specification. A short introduction can be found on https://pgjones.dev/blog/how-to-write-asgi-middleware-2021 - You can see how the CORS middleware has been implemented here: https://github.com/encode/starlette/blob/15761fb48e4c56be09167cb8f9b761114593b651/starlette/middleware/cors.py – MatsLindh Mar 18 '22 at 09:54

2 Answers2

32

As FastAPI is actually Starlette underneath, you could use BaseHTTPMiddleware that allows you to implement a middleware class (you may want to have a look at this post as well). Below are given two variants of the same approach on how to do that, where the add_middleware() function is used to add the middleware class. Please note that is currently not possible to use BackgroundTasks (if that's a requirement for your task) with BaseHTTPMiddleware—check #1438 and #1640 for more details. Alternatives can be found in this answer and this answer.

Option 1

middleware.py

from fastapi import Request

class MyMiddleware:
    def __init__(
            self,
            some_attribute: str,
    ):
        self.some_attribute = some_attribute

    async def __call__(self, request: Request, call_next):
        # do something with the request object
        content_type = request.headers.get('Content-Type')
        print(content_type)
        
        # process the request and get the response    
        response = await call_next(request)
        
        return response

app.py

from fastapi import FastAPI
from middleware import MyMiddleware
from starlette.middleware.base import BaseHTTPMiddleware

app = FastAPI()
my_middleware = MyMiddleware(some_attribute="some_attribute_here_if_needed")
app.add_middleware(BaseHTTPMiddleware, dispatch=my_middleware)

Option 2

middleware.py

from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware

class MyMiddleware(BaseHTTPMiddleware):
    def __init__(
            self,
            app,
            some_attribute: str,
    ):
        super().__init__(app)
        self.some_attribute = some_attribute

    async def dispatch(self, request: Request, call_next):
        # do something with the request object, for example
        content_type = request.headers.get('Content-Type')
        print(content_type)
        
        # process the request and get the response    
        response = await call_next(request)
        
        return response

app.py

from fastapi import FastAPI
from middleware import MyMiddleware

app = FastAPI()
app.add_middleware(MyMiddleware, some_attribute="some_attribute_here_if_needed")
Chris
  • 18,724
  • 6
  • 46
  • 80
  • 11
    For anyone that uses this approach, please make sure to read the bug about [using `BaseHTTPMiddleware`](https://www.starlette.io/middleware/#basehttpmiddleware) (The red box at the bottom). The middleware with starlette does not play well with background tasks. Just be warned because it caught us by suprise. – Error - Syntactical Remorse Jun 13 '22 at 16:35
  • @Error-SyntacticalRemorse I don't see any red box in that link. Was the problem fixed? – Pedro A Nov 10 '22 at 14:09
  • 1
    @PedroA Appears it was fixed. The [wayback machine](https://web.archive.org/web/20220630102432/https://www.starlette.io/middleware/#basehttpmiddleware) shows it but it was removed in the newest link. I will leave the comment because we recently used it again with fastapi but when we put it under extreme load, the raw middleware was faster and the BaseHTTPMiddleware appeared to have a memory leak with its Depends on checks. (That last sentence was just an opinion we had after testing, not a fact). I would try to use `BaseHTTPMiddleware` if you can! – Error - Syntactical Remorse Nov 10 '22 at 18:51
  • @chris this looks like a before request, what about after request – Mudassar Malik Aug 20 '23 at 03:06
6

A potential workaround for the BaseHTTPMiddleware bug raised by @Error - Syntactical Remorse, which seems to work for me at least, is to use partial and use a functional approach to your middleware definition:

middleware.py

from typing import Any, Callable, Coroutine
from fastapi import Response


async def my_middleware(request: Request, call_next: Callable, some_attribute: Any) -> Response:
    request.state.attr = some_attribute  # Do what you need with your attribute
    return await call_next(request)

app.py

from functools import partial
from fastapi import FastAPI
from middleware import my_middleware


app = FastAPI()

my_custom_middleware: partial[Coroutine[Any, Any, Any]] = partial(my_middleware, some_attribute="my-app")

app.middleware("http")(my_custom_middlware)
J.Aluko
  • 96
  • 1
  • 6