FastAPIのMiddleware、CustomRoute、Dependenciesの実行順序を確認してみる

2023.12.28

はじめに

データアナリティクス事業本部のkobayashiです。

FastAPIではリクエストを受けてからレスポンスを返す間にMiddlewareとCustomRouteを使って処理を挟むことができます。またDI (Dependency Injection)としてDependsメソッドを使って依存性注入を行うこともできます。今回はこれらの実行順序がどうなるかを簡単に調べてみたいと思います。

Middleware、CustomRoute、Dependenciesとは

Middlewareは、FastAPIアプリケーションのリクエストとレスポンスを処理するために使用されるコンポーネントでMiddlewareは各リクエストとレスポンス間で処理を追加できます。

Middlewareはリクエストとレスポンスを処理するためのコンポーネントですが、ルートによっては更にMiddlewareのロジックを上書きしたい場合もあります。そのような場合はCustomRouteを使います。またrequest.scope["route"].pathでパスパラメータを取得することができます。

Dependenciesは、特定のパス操作やルーティング前に事前に実行する関数です。これらの依存関数はコードの再利用、セキュリティ、データ検証などの機能に使用されます。

では実際にFastAPIのコードを記述して実行順序を確認してみたいと思います。

Middleware、CustomRoute、Dependenciesの実行順序を確認してみる

環境

  • Python: 3.11.4
  • FastAPI: 0.105.0

FastAPIでCustomRoute、Middleware、Dependenciesを記述したコードは以下になります。

from fastapi import FastAPI, Depends
from fastapi.routing import APIRoute
from starlette.middleware.base import BaseHTTPMiddleware


class CustomRoute(APIRoute):
    def get_route_handler(self):
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request):
            print("##### CustomRoute Start #####")
            response = await original_route_handler(request)
            print("##### CustomRoute Finish #####")

            return response

        return custom_route_handler


class CustomMiddleware(BaseHTTPMiddleware):

    async def dispatch(self, request, call_next):
        print("##### MiddlewareClass-1 Start #####")
        response = await call_next(request)
        print("##### MiddlewareClass-1 Finish #####")

        return response


app = FastAPI()


@app.middleware("http")
async def add_middleware(request, call_next):
    print("##### Middleware-1 Start #####")
    response = await call_next(request)
    print("##### Middleware-1 Finish #####")

    return response


app.add_middleware(CustomMiddleware)

app.router.route_class = CustomRoute


def dependencies():
    print("##### Dependencies-1 #####")
    return "Test-1"

def dependencies_ret():
    print("##### DependenciesRet-1 #####")
    return "Test-1"

@app.get("/",
         dependencies=[Depends(dependencies), ])
async def root(ret=Depends(dependencies_ret)):
    print("##### リクエストを処理してレスポンスを返す #####")
    return {"message": "Hello World"}


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=80)

MiddlwwareはMiddlewareのクラスを作成した上でadd_middlewareで追加したパターンとデコレータの@app.middleware("http")を使って追加するパターンがあるので両方設定してみます。またDependenciesもデコレータで指定するパターンと引数で指定して戻り値を使用するパターンの両方を指定してみます。

ではこれを実行してみます。

$ python main.py
##### MiddlewareClass-1 Start #####
##### Middleware-1 Start #####
##### CustomRoute Start #####
##### Dependencies-1 #####
##### DependenciesRet-1 #####
##### リクエストを処理してレスポンスを返す #####
##### CustomRoute Finish #####
##### Middleware-1 Finish #####
##### MiddlewareClass-1 Finish #####

おおよそ想像していた通りで、以下の実行順序となります。

  1. Middlewareの前処理
  2. CustomRouteの前処理
  3. Dependenciesの処理
  4. リクエストを処理してレスポンスを返す
  5. CustomRouteの後処理
  6. Middlewareの後処理

Dependenciesはデコレータに記述された処理が先に行われその後に引数として指定されたDependenciesが処理されています。なので実際にエンドポイントの処理を行う場合もこの点に気をつける必要があります。

Middlewareに関してはClassで記述した方がデコレータで記述したMiddlewareをラップして処理している形ですがこれは記述する箇所に依存しているので更に別のコードで確かめて見ます。

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


class CustomMiddleware(BaseHTTPMiddleware):

    async def dispatch(self, request, call_next):
        print("##### MiddlewareClass-1 Start #####")
        response = await call_next(request)
        print("##### MiddlewareClass-1 Finish #####")

        return response


app = FastAPI()


app.add_middleware(CustomMiddleware) # <- 先に記述

@app.middleware("http")
async def add_middleware(request, call_next): # <- 後に記述
    print("##### Middleware-1 Start #####")
    response = await call_next(request)
    print("##### Middleware-1 Finish #####")

    return response



@app.get("/")
async def root():
    print("##### リクエストを処理してレスポンスを返す #####")
    return {"message": "Hello World"}


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=80)

先程と異なり、add_middleware@app.middleware("http")の先に記述してあります。

$ python main.py
##### Middleware-1 Start #####
##### MiddlewareClass-1 Start #####
##### リクエストを処理してレスポンスを返す #####
##### MiddlewareClass-1 Finish #####
##### Middleware-1 Finish #####
INFO:     127.0.0.1:52774 - "GET / HTTP/1.1" 200 OK

以上のようにClassで記述した方がデコレータで後に記述したものにラップされて、処理の順序としてはエンドポイントでの処理前は記述した順序と逆の順序で実行され、エンドポイントでの処理の後は記述した順序で実行されています。

最後にMiddleware、Dependenciesを複数記述して順序を確かめてみたいと思います。

from fastapi import FastAPI, Depends
from fastapi.routing import APIRoute
from starlette.middleware.base import BaseHTTPMiddleware


class CustomRoute(APIRoute):
    def get_route_handler(self):
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request):
            print("##### CustomRoute Start #####")
            response = await original_route_handler(request)
            print("##### CustomRoute Finish #####")

            return response

        return custom_route_handler


class CustomMiddleware(BaseHTTPMiddleware):

    async def dispatch(self, request, call_next):
        print("##### MiddlewareClass-1 Start #####")
        response = await call_next(request)
        print("##### MiddlewareClass-1 Finish #####")

        return response


class CustomMiddleware2(BaseHTTPMiddleware):

    async def dispatch(self, request, call_next):
        print("##### MiddlewareClass-2 Start #####")
        response = await call_next(request)
        print("##### MiddlewareClass-2 Finish #####")

        return response


app = FastAPI()


@app.middleware("http")
async def add_middleware(request, call_next):
    print("##### Middleware-1 Start #####")
    response = await call_next(request)
    print("##### Middleware-1 Finish #####")

    return response


@app.middleware("http")
async def add_middleware(request, call_next):
    print("##### Middleware-2 Start #####")
    response = await call_next(request)
    print("##### Middleware-2 Finish #####")

    return response


app.add_middleware(CustomMiddleware)
app.add_middleware(CustomMiddleware2)

app.router.route_class = CustomRoute


def dependencies():
    print("##### Dependencies-1 #####")
    return "Test-1"


def dependencies2():
    print("##### Dependencies-2 #####")
    return "Test-2"


def dependencies_ret():
    print("##### DependenciesRet-1 #####")
    return "Test-1"


def dependencies_ret2():
    print("##### DependenciesRet-2 #####")
    return "Test-2"


@app.get("/",
         dependencies=[Depends(dependencies), Depends(dependencies2), ])
async def root(ret=Depends(dependencies_ret), ret2=Depends(dependencies_ret2)):
    print("##### リクエストを処理してレスポンスを返す #####")
    return {"message": "Hello World"}


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=80)

これを実行すると以下の結果となります。

$ python main.py

##### MiddlewareClass-2 Start #####
##### MiddlewareClass-1 Start #####
##### Middleware-2 Start #####
##### Middleware-1 Start #####
##### CustomRoute Start #####
##### Dependencies-1 #####
##### Dependencies-2 #####
##### DependenciesRet-1 #####
##### DependenciesRet-2 #####
##### リクエストを処理してレスポンスを返す #####
##### CustomRoute Finish #####
##### Middleware-1 Finish #####
##### Middleware-2 Finish #####
##### MiddlewareClass-1 Finish #####
##### MiddlewareClass-2 Finish #####
INFO:     127.0.0.1:54395 - "GET / HTTP/1.1" 200 OK

これを元にMiddleware、CustomRoute、Dependenciesの実行順序をまとめると以下のようになります。

  • Middleware、CustomRoute、Dependenciesの実行順序で実行される
  • Middlewareはadd_middleware@app.middleware("http")で記述した順序の後ろからラップされて処理される
    • エンドポイントでの処理前は記述した順序と逆の順序で実行される
    • エンドポイントでの処理の後は記述した順序で実行される。
  • Dependenciesはデコレータでの記述が先に実行される
    • デコレータ、引数内での順序は記述した順に実行される。

まとめ

FastAPIでMiddleware、CustomRoute、Dependenciesの実行順序をコードを使って確認してみました。この処理が順序が明確になったのでどこでどの処理を記述すべきかを考えながらエンドポイントを設計できるようになりました。

最後まで読んで頂いてありがとうございました。