---
title: "FastAPIのミドルウェアの使い方｜BaseHTTPMiddlewareと純ASGIの違い・実行順序・CORSをコードで解説"
url: "https://www.issoh.co.jp/tech/details/19743/"
published: 2026-11-14
updated: 2026-11-14
categories: ["開発"]
publisher: "株式会社一創"
---

# FastAPIのミドルウェアの使い方｜BaseHTTPMiddlewareと純ASGIの違い・実行順序・CORSをコードで解説

FastAPIのミドルウェアは、すべてのリクエストがエンドポイントに届く前と、すべてのレスポンスが返る前に、共通の処理を差し込む仕組みです。処理時間の計測、リクエストIDの付与、CORSのヘッダー、レスポンスの圧縮のように、エンドポイントごとに書きたくない処理をまとめて差し込むための置き場所です。この記事では、FastAPI 0.162.7とStarlette 1.10.0を前提に、`@app.middleware("http")`の書き方から、複数登録したときの実行順序、`BaseHTTPMiddleware`と純ASGIミドルウェアの違い、組み込みのCORS・GZipの設定で起きる事故までを、コードとソースの実物で確かめていきます。なお、OSとアプリの間で働くインフラ用語の「ミドルウェア」とは別物で、そちらは[ミドルウェアの種類と選定の解説](https://www.issoh.co.jp/tech/details/13378/)で扱っています。

## まとめ：FastAPIのミドルウェアを書く前に決めておく3つの結論

手軽に書くなら`@app.middleware("http")`で足ります。ただし、その正体は`BaseHTTPMiddleware`で、WebSocketには効かず、エンドポイントで設定した`ContextVar`も外側から読めません。リクエストIDやトレースのように、WebSocketを含む全経路で動かしたい処理や、`ContextVar`を使う処理は、純ASGIミドルウェアで書きます。

実行順序は「後から登録したものが外側」です。CORSのように、エラー応答にも付けたいヘッダーは最後に登録して外側に置きます。また、ミドルウェアの中で`HTTPException`を投げても例外ハンドラーには届かず500になるので、拒否するときは`JSONResponse`を直接返します。

組み込みのCORSで`allow_origins=["*"]`と`allow_credentials=True`を同時に指定すると、Starlette 1.10.0は要求元のOriginをそのまま許可して返します。公式ドキュメントの「併用できない」という説明とは違い、エラーにはなりません。資格情報を扱うAPIでは、許可するオリジンを必ず列挙します。

## FastAPI 0.162系でミドルウェアを動かす検証環境と最小コードの書き方

まずは1つのミドルウェアを書き、リクエストの前後で処理が動く様子をヘッダーで確かめます。

### fastapi\[standard\]0.162.7とStarlette 1.10導入・起動

2026年11月14日時点のPyPIで、[FastAPIの最新は0.162.7](https://pypi.org/project/fastapi/)（2026年11月13日公開）です。動作要件はPython 3.11以上で、土台のStarletteは1.10.0（2026年10月24日公開）が入ります。FastAPIの起動そのものが初めてなら、[FastAPIの最短の起動手順とasync/defの使い分け](https://www.issoh.co.jp/tech/details/3859/)を先に済ませてください。

```
# 仮想環境を作り、FastAPIとテスト用のpytestを入れる
python3 -m venv .venv
source .venv/bin/activate
pip install "fastapi[standard]==0.162.7" pytest

# 入った版を確かめる（starlette 1.10.0 になっているか）
pip show fastapi starlette | grep -E "^(Name|Version)"

# main.py を置いたディレクトリで開発サーバーを起動する
fastapi dev main.py
```

FastAPIの依存指定は`starlette>=0.46.0`なので、既存の環境では古いStarletteが残っていることがあります。後で触れるGZipの除外設定やボディサイズの上限はStarletteの版で挙動が変わるため、`pip show`で版を確かめてから読み進めてください。

### @app.middlewareで処理時間をヘッダーに付ける最小のコード例

[公式のミドルウェアの説明](https://fastapi.tiangolo.com/ja/tutorial/middleware/)と同じ形で、処理時間を`X-Process-Time`ヘッダーに入れるミドルウェアを書きます。

```
# main.py
import time
from collections.abc import Awaitable, Callable

from fastapi import FastAPI, Request, Response

app = FastAPI()


@app.middleware("http")
async def add_process_time(
    request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
    start = time.perf_counter()
    response = await call_next(request)  # ここでエンドポイントまで処理が進む
    response.headers["X-Process-Time"] = f"{time.perf_counter() - start:.4f}"
    return response


@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}
```

```
# 別の端末から呼び、レスポンスヘッダーを表示する
curl -i "http://127.0.0.1:8000/items/1"
```

応答ヘッダーに`x-process-time: 0.0003`のような行が付けば、ミドルウェアが動いています。存在しないパスで404が返る場合や、型が合わず422が返る場合も、ミドルウェアは同じように通ります。

### call\_nextの前後でリクエストとレスポンスを触る範囲の線引き

`call_next`より前に書いた処理はエンドポイントの前に、後ろに書いた処理はエンドポイントの後に動きます。前ではヘッダーやパス、クエリを読み、後ろではステータスコードとヘッダーを書き換えるのが基本の使い方です。

本文の書き換えは避ける方針です。`call_next`が返すレスポンスは、Starlette 1.10.0のソースではチャンク単位で流れるストリームになっていて、本文を書き換えるには全体を読み切る必要があります。大きなファイルの配信で読み切ると、その分だけメモリの消費が増える仕組みです。本文を変えたい場合は、後述の純ASGIミドルウェアで`http.response.body`のメッセージを扱い、`Content-Length`も合わせて直します。

## add\_middlewareの登録順と例外ハンドラーとの位置関係を実測で確かめる

ミドルウェアが2つ以上になると、どれが先に動くかで結果が変わります。FastAPIのソースと実測で、積み方を確かめます。

### 後から登録したミドルウェアが外側になる積み順とログでの確認方法

[Starlette 1.10.0のadd\_middleware](https://github.com/Kludex/starlette/blob/1.10.0/starlette/applications.py)は、登録したミドルウェアをリストの先頭に差し込みます。そのため、後から登録したものほど外側に来て、リクエストでは先に、レスポンスでは後に動く順序です。`@app.middleware("http")`も内部では`add_middleware`を呼ぶだけなので、デコレーターとメソッドを混ぜても同じ規則に従います。

```
app.add_middleware(MiddlewareA)  # 先に登録：内側
app.add_middleware(MiddlewareB)  # 後に登録：外側

# Starlette 1.10.0 で実測した呼び出し順
# B:in → A:in → endpoint → A:out → B:out
```

アプリが最初のリクエストを受けた時点でミドルウェアの積み方は確定し、その後に`add_middleware`を呼ぶと「Cannot add middleware after an application has started」の`RuntimeError`になります。ミドルウェアの登録は、アプリを作ったモジュールの読み込み時に済ませます。

### ミドルウェア内のHTTPExceptionが500になる理由と返し方

[FastAPI 0.162.7のbuild\_middleware\_stack](https://github.com/fastapi/fastapi/blob/0.162.7/fastapi/applications.py)は、外側から順に次のように積みます。表の例外テレメトリー層は`ExceptionTelemetryMiddleware`を指す表記です。

| 位置   | ミドルウェア                   |
| ---- | ------------------------ |
| 最も外側 | ServerErrorMiddleware    |
| 2番目  | 例外テレメトリー層                |
| 3番目  | 利用者が登録したミドルウェア           |
| 4番目  | ExceptionMiddleware      |
| 最も内側 | AsyncExitStackMiddleware |

`exception_handlers`を処理する`ExceptionMiddleware`は、利用者のミドルウェアより内側にあります。そのため、ミドルウェアの中で`raise HTTPException(403)`と書いても例外ハンドラーには届かず、外側の`ServerErrorMiddleware`が受けて500を返します。Starlette 1.10.0で確かめたところ、403のつもりの応答が「500 Internal Server Error」になりました。

```
from fastapi.responses import JSONResponse


@app.middleware("http")
async def block_by_header(request: Request, call_next):
    if request.headers.get("x-api-key") != "expected-key":
        # raise HTTPException ではなく、レスポンスを直接返す
        return JSONResponse({"detail": "Forbidden"}, status_code=403)
    return await call_next(request)
```

エンドポイントの中で起きた`HTTPException`は、内側の`ExceptionMiddleware`が通常どおり処理するため、ミドルウェアからは整形済みの応答として見えます。

### yield依存の終了処理とBackgroundTasksが走るタイミング

公式の説明では、`yield`を使う依存関係の終了処理はミドルウェアの後に、バックグラウンドタスクはすべてのミドルウェアの後に実行されます。ミドルウェアで計った処理時間には、DBセッションのクローズや送信後のメール処理は入りません。

古いStarletteでは、`BaseHTTPMiddleware`を挟むとバックグラウンドタスクの終わりまで応答が待たされる問題がありました。[Starletteのリリースノート](https://starlette.dev/release-notes/)によると、1.7.0（2026年9月）で「応答の送信後にだけバックグラウンドタスクを実行する」よう修正されています。重いタスクで応答が遅いと感じたら、まずStarletteの版を確かめます。

## BaseHTTPMiddlewareと純ASGIミドルウェアの違いと書き分けの基準

FastAPIで書けるミドルウェアには、`call_next`を使う`BaseHTTPMiddleware`型と、ASGIの`scope`・`receive`・`send`を直接扱う純ASGI型の2種類があります。

### @app.middlewareの正体がBaseHTTPMiddlewareである点と制約

FastAPI 0.162.7の`middleware()`メソッドの中身は、渡した関数を`add_middleware(BaseHTTPMiddleware, dispatch=func)`で登録する数行だけです。つまり、デコレーターで書いた関数も、`BaseHTTPMiddleware`を継承して`dispatch`を書いたクラスも、動きは同じになります。

なお、土台のStarletteは1.0.0rc1で`@app.middleware()`デコレーターを削除しました（PR 3117）。FastAPIは自前の実装としてデコレーターを残しているため、FastAPIでは今も使えますが、Starletteの素のアプリに同じコードを移すと動きません。

### ContextVarが外側へ伝わらない制約とWebSocketに効かない仕様

[Starletteのミドルウェアの説明](https://starlette.dev/middleware/)は、`BaseHTTPMiddleware`の既知の制約として、エンドポイントで変更した[contextvars.ContextVar](https://docs.python.org/3/library/contextvars.html)の値がミドルウェア側へ伝わらない点を挙げています。実測でも、エンドポイントで値を入れた`ContextVar`を`call_next`の後で読むと、既定値の`"unset"`のままでした。同じ処理を純ASGIミドルウェアで書くと、入れた値が読めます。

もう1つの制約はWebSocketです。`BaseHTTPMiddleware`は`scope["type"]`が`"http"`以外のときは何もせず素通しするため、WebSocketの接続では`dispatch`が一度も呼ばれません。認証やリクエストIDをWebSocketにも効かせたいなら、純ASGIで書くのが前提です。WebSocket側の実装は[FastAPIのWebSocket実装の解説](https://www.issoh.co.jp/tech/details/19697/)で扱っています。

| 観点         | BaseHTTPMiddleware | 純ASGI         |
| ---------- | ------------------ | ------------- |
| 書きやすさ      | call\_nextで簡単      | scopeとsendを扱う |
| WebSocket  | 呼ばれない              | 扱える           |
| ContextVar | 外側へ伝わらない           | 伝わる           |
| 本文の書き換え    | 全体の読み切りが要る         | チャンク単位で可能     |

### 純ASGIミドルウェアでリクエストIDを付与する実装とテスト

純ASGI型で、リクエストIDを付けるミドルウェアを書きます。受け取った`X-Request-ID`を使い回しますが、外部から来る値なので、英数字とハイフンの64文字以内に合わない場合は捨てて新しく発行します。改行などの制御文字をそのまま応答ヘッダーやログに流さないための形式検証です。

```
# request_id.py
import re
import uuid

from starlette.datastructures import Headers, MutableHeaders
from starlette.types import ASGIApp, Message, Receive, Scope, Send

_VALID_ID = re.compile(r"[A-Za-z0-9-]{1,64}")


class RequestIDMiddleware:
    def __init__(self, app: ASGIApp, header_name: str = "x-request-id") -> None:
        self.app = app
        self.header_name = header_name

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        # lifespan など、HTTPとWebSocket以外はそのまま通す
        if scope["type"] not in ("http", "websocket"):
            await self.app(scope, receive, send)
            return

        incoming = Headers(scope=scope).get(self.header_name, "")
        request_id = incoming if _VALID_ID.fullmatch(incoming) else uuid.uuid4().hex
        # request.state.request_id で読めるよう scope["state"] に入れる
        scope.setdefault("state", {})["request_id"] = request_id

        async def send_with_id(message: Message) -> None:
            if message["type"] == "http.response.start":
                MutableHeaders(scope=message)[self.header_name] = request_id
            await send(message)

        await self.app(scope, receive, send_with_id)
```

```
# main.py に追加
from request_id import RequestIDMiddleware

app.add_middleware(RequestIDMiddleware)


@app.get("/whoami")
async def whoami(request: Request):
    return {"request_id": request.state.request_id}
```

状態は`__call__`の中のローカル変数に閉じ、インスタンスの属性には書きません。ASGIのミドルウェアは同時に来る複数のリクエストで1つのインスタンスを共有するため、`self`に書くとリクエスト同士で値が混ざります。テストは次のように書けます。

```
# test_request_id.py
from fastapi.testclient import TestClient

from main import app


def test_request_id_is_reused_or_regenerated():
    with TestClient(app) as client:
        res = client.get("/whoami", headers={"x-request-id": "abc-123"})
        assert res.headers["x-request-id"] == "abc-123"
        assert res.json() == {"request_id": "abc-123"}

        # 形式に合わない値は捨て、32桁のIDを新しく発行する
        res = client.get("/whoami", headers={"x-request-id": "abc\tdef"})
        assert len(res.headers["x-request-id"]) == 32
```

Starlette 1.10.0単体で同じミドルウェアを動かしたところ、HTTPとWebSocketの両方でIDが入り、タブ文字を含む値は新しいIDに置き換わりました。このIDをログの各行に載せて相関を取る方法は、[FastAPIのloggingの設定方法｜uvicornのログ統合・JSON化・リクエストIDをコードで解説](https://www.issoh.co.jp/tech/details/19745/)で、ContextVarとlogging.Filterを使うコードとともに解説しています。

## CORS・GZip・TrustedHostなど組み込みミドルウェアの設定値と落とし穴

FastAPIは`fastapi.middleware`からStarletteの組み込みミドルウェアをそのまま使えるようにしています。設定値の既定と、実物で確かめた挙動を整理します。

### CORSMiddlewareの全オリジン許可と資格情報を併用する危険

[公式のCORSの説明](https://fastapi.tiangolo.com/ja/tutorial/cors/)は、`allow_credentials=True`のときは`allow_origins`などを`["*"]`にできないと書いています。ところが[Starlette 1.10.0のCORSMiddleware](https://github.com/Kludex/starlette/blob/1.10.0/starlette/middleware/cors.py)は、この組み合わせを拒否せず、要求の`Origin`をそのまま`Access-Control-Allow-Origin`に入れて返します。

```
# 実測：allow_origins=["*"], allow_credentials=True のアプリへ
curl -i -H "Origin: https://evil.example" "http://127.0.0.1:8000/"
# access-control-allow-origin: https://evil.example
# access-control-allow-credentials: true
```

これは、どのサイトからでもCookie付きでAPIの応答を読める状態です。ブラウザの制限を外す設定なので、資格情報を使うAPIでは許可するオリジンを列挙します。

```
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],  # "*" にしない
    allow_credentials=True,
    allow_methods=["GET", "POST"],
    allow_headers=["Authorization", "Content-Type"],
    expose_headers=["X-Request-ID"],  # ブラウザのJSから読ませるヘッダー
)
```

許可外のオリジンやメソッドのプリフライトには、400で返る応答は「Disallowed CORS origin」「Disallowed CORS method」です。CORSは最後に登録して最も外側に置くと、内側のミドルウェアが返した拒否応答にもCORSヘッダーが付き、ブラウザ側で原因を読めるようになります。CORSの仕組みやブラウザのエラー文の読み方は[CORSの仕組みとエラー文別の解決法](https://www.issoh.co.jp/tech/details/13763/)、Reactと組むときの設定は[React×FastAPI連携の手順](https://www.issoh.co.jp/tech/details/2242/)にまとめています。

### GZipMiddlewareのminimum\_sizeと圧縮対象外の条件

`GZipMiddleware`は、`Accept-Encoding`に`gzip`を含むリクエストの応答を圧縮します。既定の`minimum_size`は500バイト、`compresslevel`は9です。

```
from fastapi.middleware.gzip import GZipMiddleware

# 1KB未満は圧縮しない。CPUを抑えたいなら圧縮率を下げる
app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=5)
```

Starlette 1.10.0で確かめると、600バイトの応答は圧縮され、100バイトの応答と`text/event-stream`の応答は圧縮されませんでした。Server-Sent Eventsや画像・動画・zipなどは既定の除外リストに入っており、これは1.5.0（2026年8月）で追加された`exclude_content_types`で変更できます。SSEを圧縮しない扱いは0.46.0（2025年2月）からで、それより古いStarletteではSSEも圧縮の対象でした。

### TrustedHostとHTTPSRedirectを本番のプロキシ構成で使う条件

[公式の高度なミドルウェアの説明](https://fastapi.tiangolo.com/advanced/middleware/)にある`TrustedHostMiddleware`は、`Host`ヘッダーが許可リストに無いリクエストに400を返します。Hostヘッダーを使った攻撃の対策で、`Host`を信頼した認可の迂回が問題になった例は[StarletteのCVE-2026-48710の解説](https://www.issoh.co.jp/tech/details/18235/)で扱っています。

`HTTPSRedirectMiddleware`は、ロードバランサーでTLSを終端する構成で無限リダイレクトを起こしやすい部品です。アプリにはHTTPで届くため、そのままでは毎回HTTPSへ転送してしまいます。[Uvicornの設定](https://uvicorn.dev/settings/)では、`--proxy-headers`は既定で有効ですが、`X-Forwarded-Proto`を信頼するのは`--forwarded-allow-ips`（既定は127.0.0.1と::1）からの接続だけです。ロードバランサーのアドレスを許可するか、HTTPSへの転送自体をロードバランサー側で行います。

## ミドルウェアに書く処理とDependsやプロキシ層へ回す処理の判断基準

ミドルウェアは全リクエストに効く分、置き場所を誤ると影響も全体に及びます。判断の軸は「全経路に一律で効かせるか」です。

### ルーター単位で掛けたい処理をミドルウェアに置かない理由と代替

FastAPI 0.162.7の`APIRouter`には`middleware`引数がありません。[FastAPIのリリースノート](https://fastapi.tiangolo.com/release-notes/)では、0.137.0（2026年6月）の内部変更で「ルーターごとのミドルウェア」が今後可能になる機能として挙がっているだけで、まだ実装されていない状態です。

管理画面のAPIだけ認証したい、といったルーター単位の処理は、ミドルウェアでパスを文字列判定するより、`APIRouter(dependencies=[Depends(...)])`に書きます。パスの判定はprefixの変更で簡単に漏れますが、依存はルーターに付いて移動します。書き方は[APIRouterのprefixと依存の指定方法](https://www.issoh.co.jp/tech/details/19741/)、どちらに置くかの線引きは[Dependsとミドルウェアの線引きを含むDependsの解説](https://www.issoh.co.jp/tech/details/19720/)を参照してください。

### 認証・ボディ検査・レート制限をミドルウェアに入れて失敗する例

認証をミドルウェアで書くと、OpenAPIのドキュメントに認証の情報が出ず、エンドポイントごとの例外（ヘルスチェックだけ認証なし、など）がパスの一覧で管理されます。認証は`Depends`に置き、ミドルウェアは全経路に共通の処理（リクエストID・処理時間・CORS・圧縮）に絞るのが安全です。

リクエストボディを`await request.body()`で読んで検査する書き方は動きますが、`BaseHTTPMiddleware`は本文全体をメモリに保持して下流へ渡します。巨大なアップロードで使うとメモリを圧迫するため、上限はStarlette 1.6.0で入った`RequestBodyLimitMiddleware`で掛ける方法です。設定は[FastAPIでPOSTを受け取る方法](https://www.issoh.co.jp/tech/details/19737/)のボディ上限の章が設定の参照先です。レート制限は、ワーカーやコンテナが複数あるとプロセス内のカウンターでは数えられないため、API GatewayやリバースプロキシかRedisなどの共有ストアに寄せます。既存のAPIで共通処理の置き場所を整理したい、認証や流量制御の設計から相談したい場合は、[API開発・システム連携の受託開発](https://www.issoh.co.jp/service/system/api/)で受け付けています。

## よくある質問

FastAPIのミドルウェアについて、実装の途中で出てくることの多い質問に、2026年11月時点の版で答えます。

### 特定のパスだけミドルウェアの処理を飛ばすにはどうしますか？

ミドルウェアの先頭で`request.url.path`を見て、対象外なら`return await call_next(request)`で素通しします。ヘルスチェックや静的ファイルを処理時間の計測から外す、といった用途です。ただし、除外の一覧が増えるならルーター単位の処理になっている合図なので、`APIRouter`の`dependencies`に移します。

### ミドルウェアでリクエストボディを読んでもエンドポイントで使えますか？

エンドポイントでも同じ本文を使うことが可能です。Starlette 1.10.0の`BaseHTTPMiddleware`は、`dispatch`の中で`await request.body()`を呼ぶと本文をメモリに保持し、下流のエンドポイントに同じ本文を渡します。一方、`request.stream()`で読み切った場合は、下流には空の本文が渡されます。署名の検証のように本文が要る処理は、エンドポイントか依存関数で読む方が素直です。

### @app.middleware(“http”)以外の種類は指定できますか？

FastAPI 0.162.7のソースでは、引数の説明は「現在は`http`だけに対応」という記述です。WebSocketやlifespanのイベントに処理を差し込みたいなら、純ASGIミドルウェアで`scope["type"]`を見て分岐します。起動時と終了時の処理は、ミドルウェアではなく[lifespanで書く方法](https://www.issoh.co.jp/tech/details/19739/)を使います。

### ミドルウェアが多いと遅くなりますか？

1つ挟むごとに増えるのは関数呼び出しの1段で、ヘッダーを付ける程度の処理なら負担は小さく済みます。遅くなる原因の多くは中身で、`BaseHTTPMiddleware`で本文を読み切る処理、同期のDBアクセス、外部APIの呼び出しがよくある例です。ミドルウェアの中で外部へ通信するなら、`httpx.AsyncClient`を使い回して`await`で呼びます。

### Starlette 1.10でTestClientを使うと警告が出るのはなぜですか？

Starlette 1.10.0では、テストクライアントの通信に`httpx`を使うと「httpx2を入れるように」という非推奨の警告が出ます。テストはそのまま動くので、まずは警告として扱って構いません。警告を消したい場合は、使っているFastAPIとStarletteの組み合わせで推奨されるパッケージを、各ライブラリのリリースノートで確認してから入れ替えます。

## 関連記事

- [FastAPIとは？最短の起動手順とasync/defの使い分け、0.162の変更点](https://www.issoh.co.jp/tech/details/3859/)：FastAPI単体の入門と起動手順です。
- [FastAPIのDependsの使い方｜依存性注入・yield・テストの差し替えをコードで解説](https://www.issoh.co.jp/tech/details/19720/)：ミドルウェアに置かない処理の置き場所です。
- [FastAPIのrouterの使い方｜APIRouterでのファイル分割・prefix・依存・バージョニングをコードで解説](https://www.issoh.co.jp/tech/details/19741/)：ルーター単位で処理を掛ける方法です。
- [CORSとは？仕組み・プリフライト・設定例とエラー文別の解決法を3ブラウザで実測](https://www.issoh.co.jp/tech/details/13763/)：CORSMiddlewareが返すヘッダーの意味です。
- [Uvicornとは？読み方・Gunicornとの違いと起動・本番設定【0.54対応】](https://www.issoh.co.jp/tech/details/4843/)：プロキシヘッダーの信頼設定を含むサーバー側の設定です。

---

出典: [FastAPIのミドルウェアの使い方｜BaseHTTPMiddlewareと純ASGIの違い・実行順序・CORSをコードで解説](<https://www.issoh.co.jp/tech/details/19743/>)（株式会社一創）
