Dev Study
← 1行のHTMLが画面に出るまで
レッスン 5 / 8·約 9 分·導線 1行のHTMLが画面に出るまで

サーバは何を返すか — Content-Type が無いと何が起きるか

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

今度は返す側です。サーバが送り出すバイト列と、それを「どう読め」と告げるヘッダを分けて考えます。

まず結論

サーバが返すのはただのバイト列です。それを HTML として扱ってよいと告げるのが Content-Type で、本文の中身そのものに「これは HTML だ」とは書かれていません。

Content-Type が無いとき、RFC 9110 §8.3 は受け取る側に二択の MAY を与えます。中身を見る方を選んだ実装は、MIME Sniffing Standard の手順で先頭バイトから型を推定します。画面に出たのは仕様の保証ではなく、推定が当たった結果です。

? 読む前に予測

Content-Type を付けずに `<h1>Hello</h1>` の 14 バイトを返すサーバがあります。このページをブラウザで開くと何が起きるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

本文には「これは HTML だ」と書かれていない

前のレッスン『接続して送る — TCP接続の上を流れるHTTPリクエストのバイト列』で、リクエストがバイト列として送られるのを見ました。返る側も応答行・ヘッダ・空行・本文というバイト列です。

<h1>Hello</h1> と書いてあるのだから HTML だと分かるのではないか。分かりません。同じ 14 バイトは、ソースを文字として見せる text/plain でもあり得ます。同じバイト列が、扱われ方によって別の意味になるからです。

この「どう読め」を告げるのが Content-Type です。RFC 9110 §8.3 は送る側にこう課しています。

送る側は SHOULD、受け取る側は MAY の二択です。付けなくても仕様違反ではありませんが、付けなかった時点で型を決めるのは受け取る側に移ります。

具体実物

Content-Type の無い応答を、生のまま読む

例題のサーバは Content-Type を設定していません。curl -i で応答をそのまま読みます。

$ curl -si http://localhost:3000/(検証時はポート 3999 で取得した。Date は実測値)コマンド出力
HTTP/1.1 200 OK
Date: Wed, 16 Sep 2026 02:16:18 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 14
 
<h1>Hello</h1>

res.end(...) に渡したのは本文だけなのに、Node v24.21.0 が Date / Connection: keep-alive / Keep-Alive: timeout=5 / Content-Length: 14 の4行を付けています。Content-Length: 14 は本文のバイト数と一致します(res.end(buffer) なので長さが確定した)。

そして Content-Type の行はどこにもありません。curl を通さず printf 'GET / HTTP/1.1\r\nHost: localhost:3000\r\n\r\n' | nc localhost 3000 で生のバイト列を直接投げても、返る4行は同じです(検証時はポート 3999)。

受け取った側にあるのは「200 だった」「本文は 14 バイト」「型は告げられていない」だけです。

✓ ここまでの確認

Content-Type が無いとき、RFC 9110 §8.3 は受け取る側に何を認めていますか。

抽象原理

仕様が MAY にした場所に、実装の手順書がある

RFC 9110 は二択を認めるところで止まり、どちらを選ぶか、どう調べるかは書いていません。その空白を埋めるのが WHATWG MIME Sniffing Standard(Last Updated 17 July 2026)です。

まず §5.1 が、HTTP で取得したリソースの supplied MIME type を「最後の Content-Type ヘッダの値」と定め、無ければ undefined とします。同じ節は拡張子についてこう書いています。

index.html という名前は使われません。次に §7 が、supplied MIME type が undefined なら sniff-scriptable flag を no-sniff flag の逆に設定して「未知の媒体型を識別する規則」(§7.1)を実行すると定めます。

その §7.1 が先頭バイトのパターン表です。判定範囲は §5.2 の resource header、つまり「終端に達する」「1445 バイト以上読んだ」「妥当な時間が経過した」のいずれかまで読んだバイト列で、例題の 14 バイトは1つ目で読み終わります。

Content-Type の有無から、扱う型が決まるまで(MIME Sniffing Standard §5.1 / §7 / §7.1)
Content-Type あり§5.1: 最後の値を supplied MIME type にする○ その型に従う
Content-Type 無し§5.1: supplied MIME type は undefined○ §8.3 の MAY の二択へ
octet-stream とみなす§8.3 の前者を選んだ場合× 中身は見ない
中身を調べる§7: sniff-scriptable = no-sniff flag の逆で §7.1 を実行○ パターン表と突き合わせる
先頭が <H1 + 0xTT§7.1 の表の該当行に一致○ computed MIME type = text/html
↓
表示されたのは「中身を調べる」経路を通り、推定が送り手の意図と一致した結果
  • ・判定範囲は §5.2 の resource header(終端・1445バイト・妥当な時間のいずれかまで)
  • ・sniff-scriptable が unset なら、§7.1 後段は「バイナリデータバイトを含まなければ text/plain」と規定する
具体実物

`<h1>` の 4 バイトが表に一致する経路

例題の本文は 14 バイト、末尾に改行もありません。先頭4バイトは 3C 68 31 3E。§7.1 の text/html 行のうち該当するのは <!DOCTYPE HTML・<HTML・<HEAD・<SCRIPT・<IFRAME に続く6行目の <H1 です(下は抜粋)。

MIME Sniffing Standard §7.1 のパターン表(text/html の一部。Leading Bytes to Be Ignored 列は省略)表
Byte Pattern Pattern Mask Computed MIME Type Note
3C 21 44 4F 43 ... 48 54 4D 4C TT FF FF DF ... FF text/html "<!DOCTYPE HTML" + 0xTT
3C 48 54 4D 4C TT FF DF DF DF DF FF text/html "<HTML" + 0xTT
3C 48 31 TT FF DF FF FF text/html "<H1" + 0xTT
3C 50 TT FF DF FF text/html "<P" + 0xTT
3C 21 2D 2D TT FF FF FF FF FF text/html "<!--" + 0xTT

この行の Note は原文で The case-insensitive string "<H1" followed by a tag-terminating byte. 条件は2つです。

1つ目、case-insensitive。大小を区別せず突き合わせるので、<h1> の 68("h")が 48("H")と一致します。

2つ目、tag-terminating byte。§3 Terminology の定義は逐語でこうです。

<h1> の4バイト目は 3E、つまり > なので一致します。

結果として <h1>Hello</h1> は <H1 の行に一致し、computed MIME type は text/html になります。判定に使われたのは先頭の4バイトだけで、Hello も </h1> も見られていません。

この表は「HTML かどうか」を文法的に調べているのではなく、決められた並びの一覧と照合しているだけです。当たったのは、例題の最初のタグがたまたま表に載っていたからです。

✓ ここまでの確認

`<h1>Hello</h1>` の先頭バイトが MIME Sniffing Standard §7.1 の表で text/html に一致するのはなぜですか。

⇄ 2つを見比べる
具体例 A

Content-Type 無しと text/plain 明示

同じ 14 バイトでも告げ方で扱いが変わります。Content-Type が無ければ §8.3 の二択に入り、中身を調べる経路なら §7.1 の表で text/html と推定されます。text/plain を明示すれば原則そのヘッダが型を決めます。ただし値が厳密に `text/plain` など §5.1 の表の4通りなら check-for-apache-bug flag が立ち、§7.2 でバイナリと判定されると application/octet-stream に変わりえます(例題の 14 バイトは text/plain のまま)。以下は MIME Sniffing の規則上の帰結であり、ブラウザでの実測ではありません。

同じ本文に対する、告げ方の違い(規則上の帰結)表
告げ方 supplied MIME type 型を決めるのは
Content-Type 無し undefined 受け取る側(§8.3 の二択)
text/plain を明示 text/plain 送る側のヘッダ(§7.2 の例外あり)
具体例 B

HTTP キャッシュの追い出し

仕様が決めない場所を実装が埋める同じ形を、順路の第35週『キャッシュはどこに効くのか — 鍵と鮮度』で扱います。RFC 9111 が規定するのは「保存してよいか」「再利用してよいか」の2点で、容量が足りないときに何を捨てるか(追い出し)は規定していません。

規定されているもの / いないもの表
対象 規定の有無 決めるのは
保存してよいか RFC 9111 が規定 仕様
どれを捨てるか(追い出し) 置換アルゴリズムの規定なし 実装

媒体型の推定とキャッシュの追い出し。対象はまったく違いますが、仕様が MAY や未規定にした場所で実装が何をするかを決めているのは誰でしょうか。

「仕様書を読めば挙動が分かる」と言えるのはどちらか、という向きで比べてください。

この章のまとめ

  • サーバが返すのはただのバイト列。本文に「これは HTML だ」とは書かれておらず、それを告げるのが Content-Type。
  • RFC 9110 §8.3 は、送る側に SHOULD、受け取る側に MAY の二択(octet-stream とみなす / データを調べる)を規定する。
  • 例題のサーバが自動で付けたのは Date / Connection / Keep-Alive / Content-Length の4つで、Content-Type は無い。
  • 中身を調べる経路の手順は MIME Sniffing Standard にある。判定範囲は §5.2 の resource header、表は §7.1、3C 48 31 TT の行が <h1> に一致する。
  • 判定に使われたのは先頭4バイトだけ。HTTP 経由では拡張子は使われない(§5.1)。
  • 表示されたことは型が確定した証拠ではなく、仕様が MAY にした場所を実装が埋めた結果にすぎない。

この先の学びにどう繋がるか

text/html と判定されたバイト列が次に何になるのかを、次のレッスン『バイト列がDOMになる — 閉じ忘れても表示される理由』で見ます。14 バイトがどう木構造に変わり、書かなかった要素がどう補われるのかを扱います。

仕様が MAY にした場所を実装が埋める形は、順路の第35週『キャッシュはどこに効くのか — 鍵と鮮度』で、RFC 9111 が追い出しを規定していないという形でもう一度出てきます。

ヘッダとステータスコードの語彙は本コースでは扱いません。既存の「HTTPセマンティクス」(RFC 9110)と「サーバーのしくみ」のクイズ群に譲ります。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 4正解: 0

Content-Type について RFC 9110 §8.3 が送る側に課す規定はどれですか。

用語集(6語)
Content-Type
本文の媒体型を告げる応答ヘッダ。RFC 9110 §8.3 は、意図する媒体型が分かっている限り生成すべき(SHOULD)とする。無い場合、受け取る側は octet-stream とみなすか中身を調べるかを選んでよい(MAY)。
MIME sniffing
受け取った側が中身を調べて媒体型を推定・上書きする振る舞い。RFC 9110 §8.3 はこれを "risks drawing incorrect conclusions about the data" と述べる。手順は WHATWG MIME Sniffing Standard が定める。
resource header
MIME Sniffing Standard §5.2 が定めるリソース先頭のバイト列。終端・1445 バイト・妥当な時間のいずれかまで読んだもの。推定はこの範囲だけを見る。
tag-terminating byte (0xTT)
MIME Sniffing Standard §3 が定めるバイト。0x20 (SP) と 0x3E (">") の2つ。パターン表の各行は「文字列 + 0xTT」で一致を判定する。
supplied MIME type
受け取った側に与えられた媒体型。§5.1 によれば HTTP では最後の Content-Type の値で、拡張子は使わない。無ければ undefined。
charset パラメータ
Content-Type に付く文字符号化方式の指定。RFC 9110 §8.3.1 が等価として挙げる表記は text/html;charset=utf-8 など4つで、名前はいずれも charset。