From 83c1703a04a737a7d882844b405dffa1f67f2fe0 Mon Sep 17 00:00:00 2001 From: hideyukiMORI Date: Tue, 19 May 2026 23:22:46 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20CORS=20+=20Auth=20=E3=83=9F=E3=83=89?= =?UTF-8?q?=E3=83=AB=E3=82=A6=E3=82=A7=E3=82=A2=E7=99=BB=E9=8C=B2=E9=A0=86?= =?UTF-8?q?=E3=83=AB=E3=83=BC=E3=83=AB=E3=82=92=E3=83=89=E3=82=AD=E3=83=A5?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E3=81=AB=E8=BF=BD=E8=A8=98=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CORS と Auth を同時使用する場合、CORSMiddleware を Auth より後に登録して 最外側に置かないと OPTIONS preflight が 401 になる問題をドキュメント化。 framework-modules.md(EN/JA)と new-project.md に完全な登録順コード例と CORS + Auth ルールの callout を追加。 Co-Authored-By: Claude Sonnet 4.6 --- docs/how-to/new-project.md | 19 +++++++++++ docs/ja/reference/framework-modules.md | 46 ++++++++++++++++++++++++++ docs/reference/framework-modules.md | 28 ++++++++++++++-- 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/docs/how-to/new-project.md b/docs/how-to/new-project.md index 5bae9d8..b1be4c9 100644 --- a/docs/how-to/new-project.md +++ b/docs/how-to/new-project.md @@ -105,6 +105,23 @@ def create_app(settings: AppSettings | None = None) -> FastAPI: limit=settings.throttle_limit, window=settings.throttle_window, ) + # Auth middleware — registered before CORS so it sits inside the CORS layer. + if settings.bearer_token_enabled: + app.add_middleware(BearerTokenMiddleware, verifier=LocalTokenVerifier(settings.bearer_tokens)) + if settings.api_key_enabled: + app.add_middleware(ApiKeyAuthMiddleware, verifier=LocalTokenVerifier(settings.api_keys)) + # CORS must be the outermost layer — register it last. + # OPTIONS preflight requests must reach CORSMiddleware before any auth check. + # If CORSMiddleware is registered before auth middleware, the auth layer becomes + # outermost and returns 401 on preflight, breaking CORS for all browsers. + if settings.cors_enabled: + app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=settings.cors_allow_credentials, + allow_methods=settings.cors_allow_methods, + allow_headers=settings.cors_allow_headers, + ) # Convert Pydantic BaseModel validation errors to RFC 9457 Problem Details app.add_exception_handler(RequestValidationError, request_validation_error_handler) # type: ignore[arg-type] @@ -117,6 +134,8 @@ app = create_app() > **Middleware ordering note:** Starlette's `add_middleware` applies middleware in reverse registration order — the last registered becomes the outermost layer. Register `ErrorHandlerMiddleware` first so it wraps everything and catches all unhandled exceptions. +> **CORS + Auth rule**: Always register `CORSMiddleware` *after* any auth middleware. In Starlette's reverse order, "last registered = outermost" means CORS wraps auth, so browser preflight (`OPTIONS`) requests are handled before authentication. + ## 6. Run the development server ```bash diff --git a/docs/ja/reference/framework-modules.md b/docs/ja/reference/framework-modules.md index 29d3ca6..ae794b1 100644 --- a/docs/ja/reference/framework-modules.md +++ b/docs/ja/reference/framework-modules.md @@ -107,6 +107,52 @@ cfg_test = AppSettings(throttle_enabled=False) # テスト用オーバー | `RequestSizeLimitMiddleware` | `nene2.middleware.request_size_limit` | ペイロードサイズ制限 | | `ThrottleMiddleware` | `nene2.middleware.throttle` | 固定ウィンドウ レートリミット | +#### `add_middleware` 引数 + +| ミドルウェア | キーワード引数 | デフォルト | +|---|---|---| +| `ErrorHandlerMiddleware` | `debug: bool`, `domain_handlers: list[DomainExceptionHandlerProtocol] \| None` | `False`, `None` | +| `SecurityHeadersMiddleware` | *(なし)* | — | +| `RequestIdMiddleware` | *(なし)* | — | +| `RequestLoggingMiddleware` | *(なし)* | — | +| `RequestSizeLimitMiddleware` | `max_bytes: int` | `1_048_576` (1 MiB) | +| `ThrottleMiddleware` | `limit: int`, `window: int` | `60`, `60` | + +`ThrottleMiddleware` には `enabled` フラグがありません。`if settings.throttle_enabled:` でラップして制御します。 + +#### 完全な登録順(任意ミドルウェア含む) + +```python +# 登録順: 最内側から最外側へ。Starlette は逆順に実行します(最後に登録したものが最外側)。 +app.add_middleware(ErrorHandlerMiddleware, debug=settings.app_debug, domain_handlers=[...]) +app.add_middleware(SecurityHeadersMiddleware) +app.add_middleware(RequestIdMiddleware) +app.add_middleware(RequestLoggingMiddleware) +app.add_middleware(RequestSizeLimitMiddleware, max_bytes=settings.max_body_size) +if settings.throttle_enabled: + app.add_middleware(ThrottleMiddleware, limit=settings.throttle_limit, window=settings.throttle_window) +# Auth ミドルウェア — CORS より前に登録して CORS レイヤーの内側に配置する +if settings.bearer_token_enabled: + app.add_middleware(BearerTokenMiddleware, verifier=LocalTokenVerifier(settings.bearer_tokens)) +if settings.api_key_enabled: + app.add_middleware(ApiKeyAuthMiddleware, verifier=LocalTokenVerifier(settings.api_keys)) +# CORS は最外側に配置 — 必ず最後に登録する。 +# OPTIONS preflight リクエストは Auth チェックの前に CORSMiddleware に到達しなければならない。 +# CORSMiddleware を Auth より前に登録すると、Auth が最外側になり preflight が 401 になる。 +if settings.cors_enabled: + app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=settings.cors_allow_credentials, + allow_methods=settings.cors_allow_methods, + allow_headers=settings.cors_allow_headers, + ) +``` + +> **CORS + Auth ルール**: `CORSMiddleware` は Auth ミドルウェアの*後に*登録してください。 +> Starlette の逆順ルールにより「最後に登録 = 最外側」となり、CORS が Auth をラップします。 +> これによりブラウザの preflight(`OPTIONS`)リクエストが認証前に処理されます。 + --- ## nene2.auth diff --git a/docs/reference/framework-modules.md b/docs/reference/framework-modules.md index 6a690e8..e895be1 100644 --- a/docs/reference/framework-modules.md +++ b/docs/reference/framework-modules.md @@ -149,10 +149,13 @@ Starlette applies middleware in **reverse registration order** — the last regi | `RequestSizeLimitMiddleware` | `max_bytes: int` | `1_048_576` (1 MiB) | | `ThrottleMiddleware` | `limit: int`, `window: int` | `60`, `60` | -`ThrottleMiddleware` has no `enabled` flag — wrap with `if settings.throttle_enabled:` to disable it: +`ThrottleMiddleware` has no `enabled` flag — wrap with `if settings.throttle_enabled:` to disable it. + +#### Full registration order with optional middleware ```python -# Correct registration order (innermost → outermost) +# Registration order: innermost first, outermost last. +# Starlette executes in reverse — the last registered wraps all others. app.add_middleware(ErrorHandlerMiddleware, debug=settings.app_debug, domain_handlers=[...]) app.add_middleware(SecurityHeadersMiddleware) app.add_middleware(RequestIdMiddleware) @@ -160,8 +163,29 @@ app.add_middleware(RequestLoggingMiddleware) app.add_middleware(RequestSizeLimitMiddleware, max_bytes=settings.max_body_size) if settings.throttle_enabled: app.add_middleware(ThrottleMiddleware, limit=settings.throttle_limit, window=settings.throttle_window) +# Auth middleware — registered before CORS so it sits inside the CORS layer +if settings.bearer_token_enabled: + app.add_middleware(BearerTokenMiddleware, verifier=LocalTokenVerifier(settings.bearer_tokens)) +if settings.api_key_enabled: + app.add_middleware(ApiKeyAuthMiddleware, verifier=LocalTokenVerifier(settings.api_keys)) +# CORS must be the outermost layer — register it last. +# OPTIONS preflight requests must reach CORSMiddleware before any auth check. +# If CORSMiddleware is registered before auth middleware, the auth layer becomes +# outermost and returns 401 on preflight, breaking CORS for all browsers. +if settings.cors_enabled: + app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=settings.cors_allow_credentials, + allow_methods=settings.cors_allow_methods, + allow_headers=settings.cors_allow_headers, + ) ``` +> **CORS + Auth rule**: Always register `CORSMiddleware` *after* any auth middleware. +> In Starlette's reverse order, "last registered = outermost" means CORS wraps auth, +> so browser preflight (`OPTIONS`) requests are handled before authentication. + --- ## nene2.auth