「APIのContent-TypeとAcceptを正しく設計する|415・406・UTF-8の実務」の内容を表す技術イラスト
UNIT 00LAB NOTES FIG.00 · EYECATCH

APIのContent-TypeとAcceptを正しく設計する|415・406・UTF-8の実務

API連携でContent-TypeとAcceptを混同しないために、415・406・422の使い分け、JSONのUTF-8、q値とwildcard、response選択、Python検証、CAD・CSVへの再利用まで解説します。

DOC NO.EKP-00-1041
DATE
REVB
READ約 13 分

APIでは、requestとresponseのどちらにもContent-Typeが現れます。さらにclientはAcceptで受け取りたい形式を指定できます。この三つを混同すると、JSONを送ったのに415になる、CSVを希望したのにJSONが返る、文字化けした本文を正常データとして保存するといった問題が起こります。

この記事では、media typeを「拡張子の言い換え」ではなく、入力と出力の処理方法を決めるAPI契約として設計します。

目次

Content-TypeとAcceptは向きが違う

RFC 9110では、Content-Typeをmessageに含まれるrepresentationのmedia typeを示すfieldと定義しています。一方、requestのAcceptは、clientがresponseで受け取れるmedia typeの希望を伝えます。

POST /measurements HTTP/1.1
Content-Type: application/json
Accept: application/json

{
  "measurement_id": "m-001",
  "pressure_pa": 14000000
}

このrequestでは、二つのheaderが別の問いへ答えています。

header対象意味
requestのContent-Typerequest body送信したデータをどう解釈するか
requestのAcceptresponse bodyclientが受信できる形式は何か
responseのContent-Typeresponse body実際に返したデータをどう解釈するか

Content-Type: application/jsonだからresponseもJSONになるとは限りません。入力はJSON、出力はCSVというAPIも作れます。requestを解析する処理とresponse形式を選ぶ処理を分離します。

media typeをtype・subtype・parameterで読む

media typeは、基本的にtype/subtypeとparameterで構成されます。

application/json
text/csv; charset=utf-8
application/problem+json

applicationやtextがtype、jsonやcsvがsubtypeです。semicolon以降はparameterで、charsetやboundaryなどを表せます。media type、parameter名、tokenの大文字・小文字を前提にした比較は避け、仕様に沿って正規化します。一方、parameterの値まで無条件に小文字化してはいけません。

同じJSON構文でも、処理上の意味が違えば異なるmedia typeを使う場合があります。たとえばapplication/problem+jsonはProblem Details用の形式です。RFC 6839の+json suffixは、そのrepresentationがJSONの構造規則に従うことを示しますが、application/jsonと同一の業務schemaであることまでは保証しません。

requestの形式をContent-Typeで検証する

JSONだけを受け付けるendpointなら、許可する入力media typeを明示します。

POST /measurements
accepted request media type: application/json
encoding: UTF-8
schema: measurement-create version 1

処理順は次のように分けます。

Content-Typeを検証
  ↓
byte列をUTF-8としてdecode
  ↓
JSON構文を解析
  ↓
field・type・値をschema検証
  ↓
業務ruleを検証して保存

media typeが未対応なら415 Unsupported Media Typeです。JSONとして壊れていれば構文error、JSONは正しいが必須fieldや値が不正ならvalidation errorとして扱います。RFC 9110の422 Unprocessable Contentは、content typeと構文を理解できても、含まれる指示を処理できない場合に使えます。

状態例代表的なresponse
未対応media typeapplication/xmlしか送られていない415
JSON構文が不正closing braceがない400
schema違反必須field欠落、型違い422またはAPI規約上の4xx
業務競合既に確定したrecordを変更409

status codeだけでなく、エラーbodyで問題の種類と修正可能なfieldを返します。415 responseでは、受け付けるrequest media typeを文書とmachine-readableなAPI定義の両方へ記載します。

JSONの文字コードを推測しない

RFC 8259では、閉じたecosystem以外で交換するJSON textをUTF-8でencodeするよう定めています。また、application/jsonの登録にはrequired parameterもoptional parameterもありません。

API契約では次の形に固定すると明確です。

  • JSON bodyはUTF-8で送受信する
  • byte列をdecodeできなければJSON解析前に拒否する
  • byte order markの許可・拒否を決める
  • Unicode文字列と、業務上許可する文字を分けて検証する
  • responseのJSONもUTF-8でencodeする

application/json; charset=utf-8を許可する実装はありますが、parameterを付けるかどうかをclientごとの推測に任せません。自分のAPIが受け付ける表記をtestし、契約へ固定します。

Acceptでresponse形式を選ぶ

同じresourceをJSONとCSVで返せるAPIを考えます。

GET /measurements/m-001 HTTP/1.1
Accept: text/csv, application/json;q=0.8

qは相対的な希望度です。省略時は1、0は受け入れ不可を表します。この例ではclientはCSVを優先し、CSVがなければJSONも受け取れます。

Acceptは次のようなmedia rangeを取れます。

  • application/json:特定のmedia type
  • text/*:text配下の任意subtype
  • */*:任意のmedia type
  • application/json;q=0:JSONは受け入れない

より具体的なrangeが優先されます。headerを単純なsubstring検索や先頭一致で処理すると、q=0を選んだり、wildcardの優先順位を誤ったりします。productionではHTTP frameworkのcontent negotiation機能を利用し、同順位時のserver側優先順も決めます。

Acceptがない場合のdefault responseもAPI契約です。通常は最も安定した形式を一つ選び、例ではapplication/jsonとします。clientが明示したどの形式も生成できない場合は406 Not Acceptableを返す設計ができます。

Pythonで入力と出力の判定を分ける

次の例では、headerの構文解析自体はHTTP frameworkが行ったものとし、正規化済みのmedia typeとAccept候補を受け取ります。入力判定と出力選択を別関数にします。

from dataclasses import dataclass


SUPPORTED_INPUT = {"application/json"}
SUPPORTED_OUTPUT = (
    "application/json",
    "text/csv",
)


class UnsupportedMediaType(ValueError):
    pass


class NotAcceptable(ValueError):
    pass


@dataclass(frozen=True)
class MediaRange:
    value: str
    quality: float = 1.0


def validate_request_type(content_type: str | None) -> str:
    if not content_type:
        raise UnsupportedMediaType("Content-Type is required")

    media_type = content_type.split(";", 1)[0].strip().lower()
    if media_type not in SUPPORTED_INPUT:
        raise UnsupportedMediaType(media_type)
    return media_type


def specificity(media_range: str, media_type: str) -> int:
    if media_range == media_type:
        return 2
    if media_range.endswith("/*") and media_type.startswith(
        media_range[:-1]
    ):
        return 1
    if media_range == "*/*":
        return 0
    return -1


def choose_response_type(
    accepted: list[MediaRange] | None,
) -> str:
    if not accepted:
        return SUPPORTED_OUTPUT[0]

    candidates = []
    for server_order, supported in enumerate(SUPPORTED_OUTPUT):
        best_match = None
        for client_order, item in enumerate(accepted):
            media_range = item.value.lower()
            match_specificity = specificity(media_range, supported)
            if match_specificity < 0:
                continue
            match_key = (match_specificity, -client_order)
            if best_match is None or match_key > best_match[0]:
                best_match = (match_key, item)

        if best_match is None:
            continue
        item = best_match[1]
        if not 0 <= item.quality <= 1:
            raise ValueError("quality must be between 0 and 1")
        candidates.append((item.quality, -server_order, supported))

    candidates = [item for item in candidates if item[0] > 0]
    if candidates:
        return max(candidates)[2]

    raise NotAcceptable("no supported response media type")

framework層では、UnsupportedMediaTypeを415、NotAcceptableを406へ対応させます。成功時は、選んだ値と同じContent-Typeをresponseへ必ず設定します。

この例はmedia type parameterを含まない、解析済みの単純なrangeを対象としています。raw Accept headerにはparameter、引用符、複数fieldの結合規則があるため、独自のsplit(",") parserをproductionへ持ち込まないことが重要です。

responseのContent-Typeを最後まで一致させる

JSONを生成したのにtext/plainを返したり、CSV errorをJSONへ切り替えたのにtext/csvのまま返したりすると、clientは誤ったparserを選びます。

正常系と異常系の両方で確認します。

  • serializerが生成したbyte列とContent-Typeが一致する
  • JSON errorはapplication/problem+jsonなど契約した形式で返す
  • CSV responseの文字コード、delimiter、改行、headerを固定する
  • Content-Encoding: gzipなどの圧縮はmedia typeと別に扱う
  • Content-Dispositionのfilenameはmedia typeの代わりにしない

同じURLから複数形式を返し、cacheを使う場合は、responseがAcceptに依存することをcacheへ伝える設計も必要です。RFC 9110のVaryを使い、少なくともVary: Acceptを検討します。

よくある失敗

  • bodyの先頭が{ならJSONと判断する:media type契約を無視し、content sniffingの差が生まれます。
  • 拡張子だけでresponse形式を決める:URL設計とrepresentation選択が密結合になります。
  • Acceptを完全一致だけで比べる:wildcard、parameter、qualityを処理できません。
  • 何でもapplication/jsonとして受ける:JSON構文でも別schemaのmedia typeを誤処理します。
  • 415と406を逆にする:415は主にrequest content、406はresponseの選択に関する問題です。
  • errorだけtext/htmlで返す:API clientがJSON parserで失敗し、本来の原因を読めません。

セキュリティと運用

RFC 9110は、受信したtypeを無視して内容を推測するMIME sniffingが、誤判定や権限昇格などの危険につながり得ると説明しています。

  • 許可するrequest media typeをendpoint単位でallowlist化する
  • body size上限をContent-Type検証とは別に設ける
  • parserへ渡す前に文字コードとbyte数を確認する
  • 未知のparameterを拒否するか無視するか統一する
  • responseには実際のserializerと一致するtypeを設定する
  • API keyやtokenをerror bodyや通常logへ含めない

media typeが正しくても内容が安全とは限りません。JSON nesting、CSV formula、XML external entity、file parserの脆弱性などは、形式ごとの検証で防ぎます。

DB・CAD・自動化へ再利用する

Content-TypeとAcceptを明示すると、一つのresourceをJSON API、CSV export、CAD metadata、帳票へ展開できます。ただし、形式ごとに同じfieldの意味、ID、単位、NULL、日時を維持する必要があります。

API仕様には、endpointごとの入力media type、出力候補、default、文字コード、schema version、415・406・422の条件を記録します。client testでは、正常なJSONだけでなく、未指定Content-Type、未知のtype、UTF-8不正、q=0、wildcard、選択不能なAcceptも確認します。

まとめ

Content-TypeとAcceptは、bodyの意味と処理方法を機械へ伝えるデータ契約です。

  • requestのContent-Typeは送信bodyの形式を表す
  • requestのAcceptは希望するresponse形式を表す
  • responseのContent-Typeは実際に返した形式を表す
  • 未対応request media typeには415を使う
  • response候補が選べない場合は406を使える
  • JSONはUTF-8へ固定し、decode後に構文とschemaを検証する
  • q、wildcard、parameterを独自の単純parserで誤処理しない
  • serializerとresponse headerを必ず一致させる
  • 複数representationをcacheする場合はVary: Acceptを検討する

この契約をAPI gateway、application、serializer、client testで共有すれば、JSON、CSV、CAD metadataなど複数形式を追加しても、入力と出力の解釈を安定させられます。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次