404 Not Found

404 Not Found


nginx

Middleware — Intercepting and Enhancing Requests and Responses

ミドルウェアは空港のセキュリティチェックポイントのようなものです。各乗客 (リクエスト)は税関, 保安検査, 搭乗ゲートを順番に通過し, どのポイントでも通過を許可されるか停止される可能性があります。帰り道 (レスポンス)は逆の順序で通過します。

1. 学ぶ内容


2. Alice のリアルストーリー

(1) ペインポイント:API が悪意のあるトラフィック急増にさらされ, 追跡もできない

Alice が PriceTracker API を公開した後, 特定の IP アドレスが毎分5,000リクエストを送信しており, データベースの負荷が急増していることが判明しました。さらに悪いことに, Bob のフロントエンドが localhost:3000 から API を呼び出した際, ブラウザの CORS ポリシーによりリクエストがブロックされ, すべてのリクエストが失敗しました。Alice はクロスオリジンアクセスとリクエストレート制限の両方の問題を同時に解決する必要がありましたが, Flask には統一的なミドルウェア機構がありません。

(2) FastAPI ミドルウェアの解決策

FastAPI/Starlette はオニオンモデルのミドルウェアを提供し, 各レイヤーが特定の問題を解決します:CORS ミドルウェアはクロスオリジンリクエストを処理し, レート制限ミドルウェアはリクエスト頻度を制御し, ロギングミドルウェアはリクエストを記録します — これらは互いに干渉せず, 登録すればすぐに有効になります。

PYTHON
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) オニオンモデルの原理

100%
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) ▶ サンプル:ミドルウェアの登録順序と実行順序

PYTHON
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}

出力:

TEXT
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 設定

PYTHON
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=["*"],  # すべてのヘッダー
)

出力:

TEXT
# 実行成功

(2) ▶ サンプル:本番環境の CORS 設定

PYTHON
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"],
)

出力:

TEXT
# 実行成功
CORS 設定 開発環境 本番環境
allow_origins ["*"] または localhost 特定のドメイン名のリスト
allow_credentials True True (Cookie が必要な場合)
allow_methods ["*"] 必要なメソッドのみ
allow_headers ["*"] 必要なヘッダーのみ
max_age デフォルト 3600 (プレフライトキャッシュ)

5. カスタムミドルウェア

(1) リクエストタイミングミドルウェア

(1) ▶ サンプル:各リクエストの処理時間をログに記録

PYTHON
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"}]

出力:

TEXT
# 関数定義成功

(2) ▶ サンプル:リクエスト ID 注入ミドルウェア

PYTHON
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,
    }

出力:

TEXT
# 関数定義成功

(3) ▶ サンプル:API レート制限ミドルウェア (簡易版)

PYTHON
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

出力:

TEXT
# 関数定義成功

6. クラスベースのミドルウェアと ASGI Callable

(1) クラスベースミドルウェアの書き方

より複雑なミドルウェアロジックには, ASGI scope, receive, send を直接操作するクラスベースのアプローチをお勧めします。

(1) ▶ サンプル:クラスベースのリクエストロギングミドルウェア

PYTHON
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)

出力:

TEXT
# 関数定義成功

(2) ミドルウェアタイプの比較

アプローチ 対象シナリオ 利点 欠点
@app.middleware("http") シンプルなインターセプト コードが最小 HTTP のみ処理
BaseHTTPMiddleware 中程度の複雑さ 設定可能 HTTP のみ処理
純粋な ASGI クラス 完全な制御 WebSocket も処理 コードが複雑

❓ よくある質問

Q ミドルウェアの登録順序はなぜ重要ですか?
A 最後に登録されたミドルウェアが最初にリクエストを処理します (最内層)。CORS は最後に登録するべき (最外層)で, すべてのレスポンスに CORS ヘッダーが含まれるようにします。レート制限は先に登録する (内層)ことで, 拒否されたリクエストも CORS を通過します。
Q @app.middleware と add_middleware は一緒に使えますか?
A はい。ただし, @app.middleware で登録されたミドルウェアは add_middleware の前 (より高いレベル)に来ます。一貫したアプローチの使用をお勧めします。
Q 本番環境でのレート制限には何を使うべきですか?
A シンプルなインメモリレート制限はシングルプロセス環境にのみ適しています。本番環境では Redis + SlowAPI またはカスタム Redis レート制限ミドルウェアを使用してください。分散・マルチプロセス環境をサポートします。
Q CORS の allow_origins 設定で * を使ってもよいですか?
A 開発環境では問題ありませんが, 本番環境では特定のドメインを指定する必要があります。* を指定した場合, allow_credentials=True を併用できません。ブラウザが拒否します。
Q ミドルウェアでリクエストボディを変更できますか?
A はい, しかしまずボディを読み取り, 再構築する必要があります。BaseHTTPMiddleware には既知の問題 (ボディ読み取り後のストリーム枯渇)があります。複雑なシナリオでは純粋な ASGI ミドルウェアの使用をお勧めします。
Q 特定のミドルウェアを無効にするにはどうすればよいですか?
A 組み込みのトグルはありません。環境変数の使用をお勧めします:if settings.ENABLE_RATE_LIMIT: app.add_middleware(RateLimitMiddleware)

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度 ⭐):PriceTracker に CORS ミドルウェアを追加し, http://localhost:3000 からのクロスオリジンアクセスを許可してください。ブラウザで Bob のフロントエンドが API を呼び出せることを確認してください。ヒント:app.add_middleware(CORSMiddleware, ...)
  2. 応用問題 (難易度 ⭐⭐):リクエストタイミングミドルウェアを実装し, レスポンスヘッダーに X-Process-Time を追加してください。Swagger UI でレスポンスヘッダーを確認してください。ヒント:@app.middleware("http") + time.perf_counter()
  3. チャレンジ問題 (難易度 ⭐⭐⭐):IP ベースのレート制限ミドルウェアを実装してください (毎分1,000リクエスト)。制限を超えた場合, 429 ステータスコードと Retry-After レスポンスヘッダーを返し, レスポンスヘッダーに残りクォータを表示してください。ヒント:defaultdict(list) + 時間窓クリーンアップ

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%