サイトアイコン IT & ライフハックブログ|学びと実践のためのアイデア集

FastAPIの厳格なContent-Type検査:JSON API更新時の確認ポイント

JSONデータのパケットが検証ゲートを通ってAPIサーバーへ届く様子を表した抽象イラスト

FastAPI 0.132.0以降では、JSONのリクエストボディを受け取る際に、妥当なContent-Typeヘッダーを既定で要求します。

以前はヘッダーを付けずにJSONだけを送っていたクライアントでも動く場合がありましたが、更新後は同じリクエストが拒否されるため、サーバーだけを先に更新すると連携が止まるおそれがあります。

対応の基本は、検査を無効にすることではありません。

まず送信側がapplication/jsonを正しく指定しているかを調べ、正常系と異常系を契約テストに残します。

FastAPI 0.132.0で何が変わったのか

FastAPI公式リリースノートは、0.132.0でstrict_content_typeによるJSONリクエストの検査を追加し、破壊的変更として案内しています。

現在の既定値では、JSONボディを解析させるリクエストにapplication/jsonなどの妥当なJSONメディアタイプが必要です。

厳格なContent-Type検査の公式解説によると、ヘッダーがないリクエストもJSONとして扱う旧来の動作へ戻すには、FastAPI(strict_content_type=False)を明示します。

しかし、この設定は互換性のための選択肢であり、更新時に機械的に追加するものではありません。

FastAPIのリクエストボディ処理では、Pydanticモデルの型宣言を基にJSONを読み取り、値を変換し、検証し、OpenAPI用のJSON Schemaを生成します。

Content-Type検査は、このモデル検証より手前で「どの形式としてボディを解釈するか」を決める境界です。

Content-Typeが担う役割

Content-Typeは、送信したボディのメディアタイプをクライアントがサーバーへ伝えるHTTPヘッダーです。

ボディがJSONらしい文字列であっても、ヘッダーがなければ、受信側はJSONとして扱う契約を確認できません。

この区別により、形式の不一致と、JSONとして解析した後の値の不備を分けて調査できます。

リクエスト 確認する境界 実務上の扱い
application/jsonと妥当なJSON メディアタイプとモデル検証 通常のJSONリクエストとして処理する
application/*+json系と妥当なJSON 対応するJSONメディアタイプか 利用する派生メディアタイプを契約テストで確認する
Content-TypeなしでJSON文字列を送信 形式を特定できるか 既定の厳格検査では拒否される
text/plainでJSON文字列を送信 宣言と内容が一致するか JSONとしての処理を期待しない
application/jsonだが値がスキーマ違反 Pydanticモデルの検証 入力項目のエラーとして扱う

HTTP Semanticsを定めるRFC 9110では、415 Unsupported Media Typeを対象リソースが対応しない形式のため処理を拒む状態、422 Unprocessable Contentを形式と構文は理解できても内容の指示を処理できない状態として区別しています。

ただし、FastAPIが返す具体的なステータスコードとエラー本文は、バージョン、ルート定義、独自例外ハンドラーによって確認する必要があります。

公開APIで415を契約に含めるなら、フレームワークの既定動作を想像で決めず、実際のレスポンスをテストし、OpenAPIにも記載します。

厳格な検査が抑える限定的なCSRFリスク

FastAPI公式が説明する主な安全上の利点は、ローカル環境や社内ネットワークで、認証を設けず「同じネットワークからのアクセスなら信頼する」APIを動かす場面にあります。

ブラウザーは、Content-Typeを付けない単純な送信について、条件によってはCORSのプリフライトを行わないことがあります。

悪意あるページがその性質を利用してローカルAPIへJSONらしいボディを送ったとき、サーバーがヘッダーなしの内容までJSONとして受理すると、意図しない操作につながる余地が生まれます。

厳格な検査は、この特定の経路でヘッダーのないJSONを受理しないための防御になります。

一方で、「厳格なContent-Type検査があれば認証は不要」を意味しません。

インターネット公開APIでは攻撃者が任意のヘッダーを付けられるため、認証、認可、CSRF対策、CORS設定、入力検証を用途に応じて組み合わせます。

更新前に洗い出すクライアント

影響を受けやすいのは、本文を文字列やバイト列として直接送る実装です。

requests.post(..., json=payload)httpx.post(..., json=payload)のような一般的なJSON送信APIは、通常は適切なヘッダーを設定します。

それでも、ラッパー関数やプロキシでヘッダーが変わる場合があるため、送信コードだけでなく、テスト環境でサーバーが受け取るリクエストを確認します。

アプリを複数ファイルへ分割している場合は、FastAPIのプロジェクト構成で解説したように、ルーターごとの個別対応を増やす前に、アプリ生成箇所と共通テストの置き場を特定すると変更範囲を抑えられます。

最小構成で既定動作を確認する

新規実装では、strict_content_typeを指定せず、既定の厳格な検査を使います。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: int

@app.post("/items")
async def create_item(item: Item) -> Item:
    return item

クライアントは、JSONを送ることをヘッダーで明示します。

curl -i http://localhost:8000/items \
  -H "Content-Type: application/json" \
  --data '{"name":"pen","price":120}'

次の送信はボディがJSONの形でもContent-Typeがないため、互換性調査で検出したいケースです。

curl -i http://localhost:8000/items \
  --data-binary '{"name":"pen","price":120}'

レスポンスのコードだけでなく、エラー本文がクライアントの扱える形式か、監視で原因を識別できるかも確認します。

回帰テストに残す四つの入力

フレームワーク更新時には、成功するリクエストだけでなく、誤ったヘッダーも固定テストにします。

from fastapi.testclient import TestClient

client = TestClient(app)

def test_accepts_json_request() -> None:
    response = client.post("/items", json={"name": "pen", "price": 120})
    assert response.status_code == 200

def test_rejects_body_without_content_type() -> None:
    response = client.post(
        "/items",
        content=b'{"name":"pen","price":120}',
    )
    assert 400 <= response.status_code < 500

def test_rejects_text_plain_for_json_endpoint() -> None:
    response = client.post(
        "/items",
        content=b'{"name":"pen","price":120}',
        headers={"Content-Type": "text/plain"},
    )
    assert 400 <= response.status_code < 500

def test_rejects_invalid_model_value() -> None:
    response = client.post("/items", json={"name": "pen", "price": "free"})
    assert 400 <= response.status_code < 500

ステータスコードとエラー構造を公開契約にしているAPIでは、範囲ではなく期待する値とJSON構造を明示して固定します。

型定義、入力検証、テストの役割分担は、FastAPIで壊れにくいAPIを作る方法も参照してください。

strict_content_type=Falseを使う判断

修正できない既存クライアントがあり、移行期間を確保する必要がある場合に限り、互換設定を検討します。

from fastapi import FastAPI

app = FastAPI(strict_content_type=False)

この設定では、Content-Typeがないリクエストボディも旧来と同様にJSONとして解析されます。

そのため、採用時には対象クライアント、終了条件、撤去予定、代替の認証とネットワーク制御を記録します。

全体設定を緩める前に、送信側へヘッダーを追加できないか、互換用の入口を限定できないかを検討すると、影響範囲を小さくできます。

移行を安全に進めるチェックリスト

  1. 利用中のFastAPIバージョンと更新先を固定し、リリースノートを確認する。
  2. JSONを送るブラウザー、モバイルアプリ、バッチ、Webhook、機器を一覧にする。
  3. Content-Typeなし、誤ったメディアタイプ、妥当なJSON、スキーマ違反をテストする。
  4. クライアント側でapplication/jsonを明示し、ステージング環境で実通信を確認する。
  5. ローカルAPIと社内APIの認証、認可、CORS、ネットワーク境界を再点検する。
  6. 更新後の4xx応答をメディアタイプ別に観測し、修正漏れを特定する。
  7. 互換設定を使う場合は撤去条件を決め、恒久設定にしない。

厳格なContent-Type検査は小さな変更に見えますが、APIの入口で曖昧な解釈を減らします。

送信側の宣言、受信側の解析、Pydanticによる値の検証を別々の境界としてテストすれば、更新時の不具合を原因ごとに切り分けられます。

出典

モバイルバージョンを終了