- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- 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 で応答をそのまま読みます。
HTTP/1.1 200 OKDate: Wed, 16 Sep 2026 02:16:18 GMTConnection: keep-aliveKeep-Alive: timeout=5Content-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つ目で読み終わります。
- ・判定範囲は §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 です(下は抜粋)。
Byte Pattern Pattern Mask Computed MIME Type Note3C 21 44 4F 43 ... 48 54 4D 4C TT FF FF DF ... FF text/html "<!DOCTYPE HTML" + 0xTT3C 48 54 4D 4C TT FF DF DF DF DF FF text/html "<HTML" + 0xTT3C 48 31 TT FF DF FF FF text/html "<H1" + 0xTT3C 50 TT FF DF FF text/html "<P" + 0xTT3C 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 に一致するのはなぜですか。
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 の例外あり)
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)と「サーバーのしくみ」のクイズ群に譲ります。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
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。