FastAPIで、Trailing Slashリクエストの自動リダイレクトを無効にする

Trailing Slashのリダイレクト動作の制御

FastAPIを含む多くのフレームワークで、末尾にスラッシュのある(Trailing Slash)リクエストを受け取ったときにマッチするurlが見つからないと、 自動で末尾にスラッシュの無いurlにリダイレクトします。

しかし、APIの開発においては、エンドポイントは正確に指定すべきだ、という考え方もあり、 今回はその考え方に則り、 FastAPIのバージョン0.98.0で有効になったredirect_slashes=Falseオプションを使って、自動リダイレクトを無効に設定してみます。

自動リダイレクトを無効にすることで、正確にエンドポイントを指定しないと、404エラーを出すようにすることが狙いです。

FastAPIのバージョン0.98.0のインストール

まず、FastAPIのバージョン0.98.0をインストールします

$ pip install fastapi==0.98.0

インストール後、pip listでバージョンを確認します

$ pip list
Package           Version
----------------- ------------
(略)
fastapi           0.98.0
(略)

インストールできました。 次は、エンドポイントを設定します。

末尾にスラッシュのないエンドポイントの設定

末尾にスラッシュのないエンドポイントを用意します

@app.get("/hello")
async def say_hello():
    return {"message": "Hello!"}

アプリケーションを起動します

$ uvicorn main:app --reload

Trailing Slashのリダイレクト動作のテスト

"http://localhost:8000/hello"を叩いてみます

INFO:     127.0.0.1:65240 - "GET /hello HTTP/1.1" 200 OK

次に末尾にスラッシュがある "http://localhost:8000/hello/"を叩いてみると、

INFO:     127.0.0.1:65261 - "GET /hello/ HTTP/1.1" 307 Temporary Redirect
INFO:     127.0.0.1:65261 - "GET /hello HTTP/1.1" 200 OK

自動でリダイレクトがかかっていることが分かります。 FastAPIはredirect_slashes=Trueがデフォルト設定になっているからです。

redirect_slashesオプションの設定変更

Trailing Slashの自動リダイレクトを無効にするには、 FastAPIのインスタンスを作成する際に、redirect_slashes=Falseにします。

app = FastAPI(redirect_slashes=False)

@app.get("/hello")
async def say_hello():
    return {"message": "Hello!"}

これで "http://localhost:8000/hello/"を叩いてみると、

INFO:     127.0.0.1:65394 - "GET /hello/ HTTP/1.1" 404 Not Found

になります。 これで、開発段階でも正確にエンドポイントを叩くことを強制することができます。

最後に、FastAPIがどのようにredirect_slashesオプションを実装しているのか確認してみます。

redirect_slashesオプションの実装を確認する

FastAPIのソースコードを見ると、applications.pyファイル内でredirect_slashesオプションが定義されています。
FastAPIのアプリケーションインスタンスが生成される際、redirect_slashesオプションはコンストラクタ引数として与えられ、FastAPIの内部でStarletteのルーターオブジェクトを作成するときに、このオプションがルーターのコンストラクタに渡され実際のリダイレクト動作を制御しているようです。

Starletteには、2019年にはredirect_slashesオプションが導入されており、FastAPIのバージョン0.98.0でFastAPIから制御できるようになったようです。 Version 0.13 by tomchristie · Pull Request #704 · encode/starlette · GitHub

FastAPI

# fastapi/applications.py
class FastAPI(Starlette):
    def __init__(
        self: AppType,
        redirect_slashes: bool = True,
        redirect_slashes=redirect_slashes
# (略)
    self.router: routing.APIRouter = routing.APIRouter(
        routes=routes,
        redirect_slashes=redirect_slashes,

Starlette

# starlette/routing.py

class Router:
    def __init__(
        self,
       ...
    ) -> None:
        self.redirect_slashes = redirect_slashes

参考

ソースコード

GitHub - kei-kmj/redirect_slashes_test