Middleware — Intercepting and Enhancing Requests and Responses
ミドルウェアは空港のセキュリティチェックポイントのようなものです。各乗客 (リクエスト)は税関, 保安検査, 搭乗ゲートを順番に通過し, どのポイントでも通過を許可されるか停止される可能性があります。帰り道 (レスポンス)は逆の順序で通過します。
1. 学ぶ内容
- Starlette ミドルウェアの仕組み:ASGI Callable ラッパーチェーン
- 組み込みミドルウェア:
CORSMiddleware設定とセキュリティポリシー - カスタムミドルウェア:リクエストタイミング, リクエスト ID 注入, レスポンスヘッダー注入
- ミドルウェアの実行順序:登録順序と実際の実行順序の関係
- Alice シナリオ:PriceTracker にリクエストロギングミドルウェアと API レート制限ミドルウェアを追加
2. Alice のリアルストーリー
(1) ペインポイント:API が悪意のあるトラフィック急増にさらされ, 追跡もできない
Alice が PriceTracker API を公開した後, 特定の IP アドレスが毎分5,000リクエストを送信しており, データベースの負荷が急増していることが判明しました。さらに悪いことに, Bob のフロントエンドが localhost:3000 から API を呼び出した際, ブラウザの CORS ポリシーによりリクエストがブロックされ, すべてのリクエストが失敗しました。Alice はクロスオリジンアクセスとリクエストレート制限の両方の問題を同時に解決する必要がありましたが, Flask には統一的なミドルウェア機構がありません。
(2) FastAPI ミドルウェアの解決策
FastAPI/Starlette はオニオンモデルのミドルウェアを提供し, 各レイヤーが特定の問題を解決します:CORS ミドルウェアはクロスオリジンリクエストを処理し, レート制限ミドルウェアはリクエスト頻度を制御し, ロギングミドルウェアはリクエストを記録します — これらは互いに干渉せず, 登録すればすぐに有効になります。
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["http://localhost:3000"])
(3) 成果
Bob はわずか5行のコードでフロントエンドのクロスドメイン問題を解決しました。レート制限ミドルウェアにより, 悪意のある IP からのリクエストは毎分5,000から1,000に減少し, ロギングミドルウェアは各リクエストにユニーク ID を割り当て, 追跡が容易になりました。
3. ミドルウェアのオニオンモデル
(1) オニオンモデルの原理
flowchart TD
Request[Client Request] --> CORS[CORS Middleware]
CORS --> Logging[Logging Middleware]
Logging --> RateLimit[Rate Limit Middleware]
RateLimit --> App[FastAPI Application]
App --> RateLimit2[Rate Limit Response]
RateLimit2 --> Logging2[Logging Response]
Logging2 --> CORS2[CORS Response]
CORS2 --> Response[Client Response]
RateLimit -.->|429 Too Many Requests| Reject[Short-circuit Rejection]
Reject --> Logging2
| 特徴 | 説明 |
|---|---|
| リクエスト方向 | 外側から内側へ (最初に登録されたものが最外層) |
| レスポンス方向 | 内側から外側へ (リクエストと逆) |
| ショートサーキット機能 | どのミドルウェアでも早期にレスポンスを返せる |
| 登録順序 | 最後に登録されたミドルウェアが最初にリクエストを処理 |
(1) ▶ サンプル:ミドルウェアの登録順序と実行順序
from fastapi import FastAPI, Request
import time
app = FastAPI()
# 最初に登録されたミドルウェア = 最外層 (リクエストで最初に実行)
@app.middleware("http")
async def outer_middleware(request: Request, call_next):
print("Outer: before request")
response = await call_next(request)
print("Outer: after response")
return response
# 最後に登録されたミドルウェア = 最内層 (リクエストで最後に実行)
@app.middleware("http")
async def inner_middleware(request: Request, call_next):
print("Inner: before request")
response = await call_next(request)
print("Inner: after response")
return response
@app.get("/test")
async def test():
print("Handler: processing")
return {"ok": True}
出力:
Outer: before request
Inner: before request
Handler: processing
Inner: after response
Outer: after response
4. CORS ミドルウェア
(1) クロスドメインリソース共有の設定
CORS (Cross-Origin Resource Sharing)は, 異なるオリジンの Web ページが API にアクセスするのを制限するブラウザのセキュリティポリシーです。Bob のフロントエンド (localhost:3000)が Alice の API (localhost:8000)にアクセスする場合, これはクロスオリジンリクエストとなります。
(1) ▶ サンプル:開発環境の CORS 設定
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # Bob のフロントエンド
allow_credentials=True,
allow_methods=["*"], # すべての HTTP メソッド
allow_headers=["*"], # すべてのヘッダー
)
出力:
# 実行成功
(2) ▶ サンプル:本番環境の CORS 設定
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
app = FastAPI()
ALLOWED_ORIGINS = [
"https://pricetracker.example.com",
"https://admin.pricetracker.example.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
出力:
# 実行成功
| CORS 設定 | 開発環境 | 本番環境 |
|---|---|---|
allow_origins |
["*"] または localhost |
特定のドメイン名のリスト |
allow_credentials |
True | True (Cookie が必要な場合) |
allow_methods |
["*"] |
必要なメソッドのみ |
allow_headers |
["*"] |
必要なヘッダーのみ |
max_age |
デフォルト | 3600 (プレフライトキャッシュ) |
5. カスタムミドルウェア
(1) リクエストタイミングミドルウェア
(1) ▶ サンプル:各リクエストの処理時間をログに記録
from fastapi import FastAPI, Request
import time
app = FastAPI()
@app.middleware("http")
async def timing_middleware(request: Request, call_next):
start_time = time.perf_counter()
response = await call_next(request)
process_time = time.perf_counter() - start_time
response.headers["X-Process-Time"] = f"{process_time:.4f}s"
return response
@app.get("/products")
async def list_products():
return [{"id": 1, "name": "Widget"}]
出力:
# 関数定義成功
(2) ▶ サンプル:リクエスト ID 注入ミドルウェア
import uuid
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def request_id_middleware(request: Request, call_next):
request_id = str(uuid.uuid4())
request.state.request_id = request_id # リクエストステートに付与
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
@app.get("/health")
async def health_check(request: Request):
return {
"status": "healthy",
"request_id": request.state.request_id,
}
出力:
# 関数定義成功
(3) ▶ サンプル:API レート制限ミドルウェア (簡易版)
from fastapi import FastAPI, Request, HTTPException
from collections import defaultdict
import time
app = FastAPI()
# シンプルなインメモリレートリミッター
rate_limits: dict[str, list[float]] = defaultdict(list)
RATE_LIMIT = 1000 # 1分あたりのリクエスト数
WINDOW = 60 # 秒
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
client_ip = request.client.host if request.client else "unknown"
now = time.time()
# 古いエントリをクリーンアップ
rate_limits[client_ip] = [
t for t in rate_limits[client_ip] if now - t < WINDOW
]
# 制限をチェック
if len(rate_limits[client_ip]) >= RATE_LIMIT:
raise HTTPException(
status_code=429,
detail=f"Rate limit exceeded: {RATE_LIMIT} requests per {WINDOW}s",
)
rate_limits[client_ip].append(now)
response = await call_next(request)
response.headers["X-RateLimit-Limit"] = str(RATE_LIMIT)
response.headers["X-RateLimit-Remaining"] = str(
RATE_LIMIT - len(rate_limits[client_ip])
)
return response
出力:
# 関数定義成功
6. クラスベースのミドルウェアと ASGI Callable
(1) クラスベースミドルウェアの書き方
より複雑なミドルウェアロジックには, ASGI scope, receive, send を直接操作するクラスベースのアプローチをお勧めします。
(1) ▶ サンプル:クラスベースのリクエストロギングミドルウェア
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
import logging
import time
logger = logging.getLogger("pricetracker")
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
start = time.perf_counter()
logger.info(f"Request: {request.method} {request.url.path}")
response = await call_next(request)
duration = time.perf_counter() - start
logger.info(
f"Response: {request.method} {request.url.path} "
f"status={response.status_code} duration={duration:.4f}s"
)
return response
# クラスベースミドルウェアの登録
app = FastAPI()
app.add_middleware(LoggingMiddleware)
出力:
# 関数定義成功
(2) ミドルウェアタイプの比較
| アプローチ | 対象シナリオ | 利点 | 欠点 |
|---|---|---|---|
@app.middleware("http") |
シンプルなインターセプト | コードが最小 | HTTP のみ処理 |
BaseHTTPMiddleware |
中程度の複雑さ | 設定可能 | HTTP のみ処理 |
| 純粋な ASGI クラス | 完全な制御 | WebSocket も処理 | コードが複雑 |
❓ よくある質問
@app.middleware で登録されたミドルウェアは add_middleware の前 (より高いレベル)に来ます。一貫したアプローチの使用をお勧めします。allow_origins 設定で * を使ってもよいですか?* を指定した場合, allow_credentials=True を併用できません。ブラウザが拒否します。BaseHTTPMiddleware には既知の問題 (ボディ読み取り後のストリーム枯渇)があります。複雑なシナリオでは純粋な ASGI ミドルウェアの使用をお勧めします。if settings.ENABLE_RATE_LIMIT: app.add_middleware(RateLimitMiddleware)📖 まとめ
- ミドルウェアはオニオンモデルに従います:リクエストは外側から内側へ, レスポンスは内側から外側へ進み, どのレイヤーでも早期にレスポンスを返せます
CORSMiddlewareでクロスドメイン問題を解決しますが, 本番環境では特定のドメイン名を指定する必要があります- カスタムミドルウェアでリクエストタイミング, リクエスト ID 注入, レート制限などの横断的関心事を実装できます
- ミドルウェアの登録順序が実行順序を決定します:最後に登録されたものが最初にリクエストを処理します (最内層)
- クラスベースミドルウェア (
BaseHTTPMiddleware)は複雑なロジックに適し, 純粋な ASGI クラスは WebSocket も処理できます
📝 練習問題
- 基本問題 (難易度 ⭐):PriceTracker に CORS ミドルウェアを追加し,
http://localhost:3000からのクロスオリジンアクセスを許可してください。ブラウザで Bob のフロントエンドが API を呼び出せることを確認してください。ヒント:app.add_middleware(CORSMiddleware, ...) - 応用問題 (難易度 ⭐⭐):リクエストタイミングミドルウェアを実装し, レスポンスヘッダーに
X-Process-Timeを追加してください。Swagger UI でレスポンスヘッダーを確認してください。ヒント:@app.middleware("http")+time.perf_counter() - チャレンジ問題 (難易度 ⭐⭐⭐):IP ベースのレート制限ミドルウェアを実装してください (毎分1,000リクエスト)。制限を超えた場合, 429 ステータスコードと
Retry-Afterレスポンスヘッダーを返し, レスポンスヘッダーに残りクォータを表示してください。ヒント:defaultdict(list)+ 時間窓クリーンアップ
---|



