DESIGN / CODE / INFRA / AI

解説

Content-Typeを付ければJSONが返る?Acceptと送る本文の種類を分けよう

要求のAcceptとContent-Type、応答のContent-Typeを整理。ローカルの小さなHTTPサーバーとcurlでJSON・テキスト・406の違いを確かめる。

Content-Typeを付ければJSONが返る?Acceptと送る本文の種類を分けよう
保存リストを見る →
この記事の道しるべ 5項目

APIへJSONを返してほしくて、要求にContent-Type: application/jsonを付けた。それだけで返事もJSONになるのかな。送る本文の種類と、受け取りたい種類を分けて考えてみよう。

この記事で扱うのは、クライアントが送る要求のAccept。HTTPヘッダーは通信の補足情報で、同じ名前でも要求側と応答側のどちらにあるかを見ることが大切だよ。

三つの役割を先に整理する

場所と名前 伝えていること
要求のAccept 応答として受け取れるメディアタイプ
要求のContent-Type 送っている本文のメディアタイプ
応答のContent-Type 返された本文のメディアタイプ

メディアタイプは、application/jsonやtext/plainのように内容の形式を表す名前。たとえばJSONの本文をPOSTで送り、処理結果を普通のテキストで受け取るAPIも考えられる。このとき要求のContent-Typeはapplication/json、Acceptはtext/plainで、両者が同じである必要はないんだ。

Content-Typeを付けても、実際の文字列が別形式へ変換されるわけではないよ。応答についてもヘッダーと本文を両方確認しよう。

二つの形式だけ返す練習先を用意する

Python 3とcurlが使える端末で、空の練習用フォルダーへaccept-practice.pyを新しく保存する。同名の自分のファイルを上書きしないようにしてね。

from http.server import BaseHTTPRequestHandler, HTTPServer
class Demo(BaseHTTPRequestHandler):
    def do_GET(self):
        accept = self.headers.get("Accept", "")
        if accept == "application/json":
            status, kind, body = 200, "application/json", b'{"name":"demo"}\n'
        elif accept == "text/plain":
            status, kind, body = 200, "text/plain", b"name=demo\n"
        else:
            status, kind, body = 406, "text/plain", b"Use application/json or text/plain\n"
        self.send_response(status)
        self.send_header("Content-Type", kind)
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Vary", "Accept")
        self.end_headers()
        self.wfile.write(body)
    def log_message(self, *_):
        pass
if __name__ == "__main__":
    HTTPServer(("127.0.0.1", 8765), Demo).serve_forever()
python3 accept-practice.py

このサーバーは127.0.0.1だけで待ち受ける。8765番が使用中なら、コードと次のURLの両方を空いている番号へ合わせよう。停止するときは起動した端末でCtrl+Cを押す。

これはAcceptの値が二つの固定文字列のどちらかかを調べる練習。ワイルドカード、複数候補、重みqを解釈する一般的なコンテンツネゴシエーションの実装ではないよ。

curlで応答の種類と本文を読む

サーバーを動かしたまま、別の端末で次を実行する。-Hは要求ヘッダー、-iは応答ヘッダーも表示する指定だよ。

curl -i -H 'Accept: application/json' http://127.0.0.1:8765/
curl -i -H 'Accept: text/plain' http://127.0.0.1:8765/
curl -i -H 'Accept: text/csv' http://127.0.0.1:8765/

最初は200とContent-Type: application/json、本文は{"name":"demo"}。二つ目は200とtext/plain、本文はname=demoになる。三つ目はこの作例では用意していない形式なので、406と案内文を返す。

応答のVary: Acceptは、応答の選択に要求のAcceptを使っていることをキャッシュへ伝える情報。実際のキャッシュ動作は、この小さなサーバーの実験だけでは確認していないよ。

送り方のラベルで返事を決めない

curl -i -H 'Accept: text/plain' \
  -H 'Content-Type: application/json' http://127.0.0.1:8765/

このGET要求には送信本文を付けていない。この作例では要求のContent-Typeを応答選択に使っていないので、Acceptで選んだtext/plainが返る。JSONを受け取る指定の代わりにContent-Typeを付けても、返事がJSONになる仕組みにはならないと確かめられるね。

Acceptは返事の保証ではない

実際のサーバーはAcceptを考慮せず、既定の形式を返す場合もある。要求に合う形式がなく、既定の表現も返さない場合の状態が406。今回の三つ目の結果から「希望と違うと必ず406になる」と広げないようにしよう。

認証が必要なAPI、間違ったURL、途中の転送などでは、期待と違う内容になることもある。使うAPIの仕様、応答番号、応答のContent-Type、本文を順に読む。Acceptを付けたことだけでJSONとして解析せず、実際に返った結果を確認してね。

編集部では2026年11月1日にMDNとRFCを確認し、上のローカル例でJSON・テキスト・406、要求Content-Typeを加えた場合の状態と本文をcurlで照合した。応答の読み分けは200でもJSONを読めない場合、キャッシュの条件はVaryの役割も参考にしよう。

参考・出典

公開と変更の記録
  • :初回公開

日時は日本時間です。記載のある変更は、確認できた編集記録に基づきます。更新日の違いだけから変更内容を推測して載せることはありません。

更新・訂正の方針 →

みんなのコメント 0

感想や、試してみたことを聞かせてね。コメント・返信は承認後に公開されます。

最初のコメントをどうぞ。