02. HTTP の進化 - 1.0 から 3.0 までの30年

読み物: 移動中・休憩中にスマホで読む用 所要時間: 40-60分 前提知識: 不要(TCP/IP の基礎があるとなお良い → IP の歴史と仕組み読後感: ブラウザのアドレスバーに URL を打って Enter を押した瞬間、世界中の網の目で何が起きているかを物語として語れる

この記事で持って帰れること

  • HTTP/0.9 から HTTP/3 まで、30年の進化を「なぜ姿を変えたか」の物語として語れる
  • HTTP/2 がバイナリ化した理由、HTTP/3 が TCP を捨てた理由を一言で説明できる
  • ステータスコード・メソッド・Cookie の設計裏側を踏まえて API を設計できる
  • 「URL を入力してから画面が出るまで」の旅を、各レイヤーを名指しで実況できる

前回 (01_TCP_IP) との繋がり

前回は「TCP/IP がインターネットの土台」という話をした。今回はその上で動く最大のプロトコル HTTP の話。TCP の弱点 (HOL ブロッキング、握手の遅さ) が、HTTP/3 が UDP に乗り換えた直接の動機 になっている。前回読んでなくても理解できるが、TCP 編を読んだ後だと「あの話、ここに繋がるのか」が腹落ちする。


はじめに: 30年で世界を変えたプロトコル

1989年、スイスのジュネーブにある CERN(欧州原子核研究機構)の地下に、Tim Berners-Lee(ティム・バーナーズ=リー)という英国人物理学者がいた。彼は研究者同士の文献共有に困っていた。論文は紙、メモはバラバラ、結果データはサーバーごとに違う形式。

彼が出した解決策は、後に世界を変える3つの発明だった。

  1. HTML - 文章にリンクを埋め込む記法
  2. URL - インターネット上のリソースに一意な住所を与える仕組み
  3. HTTP - その住所のリソースを取りに行くための手順

この記事は、この3つ目の HTTP(HyperText Transfer Protocol) が、30年でどう姿を変え、なぜ姿を変えたか、その変遷の物語である。

この記事の流れ

  1. HTTP/0.9(1991)- 1行だけのプロトコル
  2. HTTP/1.0(1996)- ヘッダの誕生
  3. HTTP/1.1(1997)- 仮想ホスト革命と長寿命接続
  4. HTTP/2(2015)- バイナリ化と多重化
  5. HTTP/3(2022)- TCP を捨てた理由
  6. ステータスコード、メソッド、Cookie の設計裏側
  7. REST、GraphQL、tRPC の流転
  8. 1リクエストの旅

第1章: 始まり - HTTP/0.9 の素朴な設計(1991)

CERN の地下から始まった

Tim Berners-Lee は1989年3月、CERN の上司に「Information Management: A Proposal」というメモを提出した。上司の手書きコメントが残っている。

“Vague, but exciting.”(漠然としているが、面白い)

このメモから2年後の1991年、世界初の Web ページが公開される。サーバーは Tim 自身の NeXT ワークステーション。URL は http://info.cern.ch/。この時のプロトコルが HTTP/0.9 だった。

0.9 はこれだけ

HTTP/0.9 の通信は、これで全部だ。

GET /index.html

そしてサーバーは HTML をそのまま返す。終わり。

HTTP/0.9 の特徴

  • メソッドは GET だけ(書き込み手段がない、純粋な閲覧プロトコル)
  • ヘッダなし(コンテンツの種類も日付も送れない)
  • ステータスコードなし(200 も 404 も無い、成功なら HTML が返り、失敗なら何も返らない)
  • HTML 専用(画像も動画も想定していない、テキスト1択)

「文書をリンクで繋ぐ」という1点に絞り切った、極めて簡素な設計。

なぜ簡素にしたか

Tim が後年語ったところによると、簡素さは戦略だった。「複雑なものは普及しない」。当時の Internet には Gopher、WAIS、FTP など競合プロトコルがあり、どれも仕様が肥大化していた。

単純なものが勝つ - インターネット普及の鉄則

Gopher は階層メニュー、WAIS は検索特化、FTP はファイル転送特化。それぞれ「特定の用途には HTTP より優れている」と主張できた。だが HTTP だけが「ハイパーリンクで文書を繋ぐ」という最も素朴な発想を貫いた。結果、HTTP は他を駆逐した。

この「単純なものが勝つ」というパターンは、後に SQL、JSON、Markdown、REST など何度も繰り返される。複雑な競合(XML、SOAP、CORBA)に対して、簡素な解が勝つ。


第2章: ヘッダの誕生 - HTTP/1.0(1996)

Web が爆発した1993年

1993年、イリノイ大学の学生 Marc Andreessen(後の Netscape 創業者)が Mosaic という Web ブラウザを開発した。画像をテキストと並べて表示できる初めてのブラウザだった。これが Web 普及の起爆剤になる。

1991年に公開された Web サイトは1つ。1993年末には600。1994年末には1万。1996年には10万を超えた。

HTTP/0.9 では限界だった。理由は3つ。

  1. 画像が扱えない(HTML しか返せない)
  2. クライアントの能力を伝える手段がない(古いブラウザに新機能を送ったら壊れる)
  3. エラーの種類が区別できない(404 なのか 500 なのか分からない)

HTTP/1.0 で導入されたもの

1996年5月、RFC 1945 として HTTP/1.0 が公開された。0.9 との違いはこうだ。

GET /index.html HTTP/1.0
Host: www.example.com
User-Agent: Mozilla/2.0
Accept: text/html, image/gif

そして応答も変わる。

HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 1234
Date: Wed, 15 May 1996 12:00:00 GMT

<html>...</html>

HTTP/1.0 の革新

  • ステータスコード(200, 404, 500 などの3桁数字)
  • ヘッダ(メタデータをキー:値で送る仕組み)
  • POST メソッド(フォーム送信、つまり「ユーザーがデータを送る」が可能に)
  • MIME タイプContent-Type: image/jpeg のように内容物を宣言できる)

ヘッダの導入が、後の Web の拡張性を支える基盤になった。Cookie も、認証も、CORS も、すべてヘッダの上に乗っている。

1.0 の致命的な弱点

HTTP/1.0 には大きな問題があった。1リクエストごとに TCP コネクションを張り直すことだ。

[ブラウザ]                              [サーバー]
   |---- TCP 接続要求 ---->|
   |<--- 接続確立 --------|
   |---- GET /page.html ->|
   |<--- HTML 返却 -------|
   |---- TCP 切断 ------->|

   |---- TCP 接続要求 ---->|  ← 画像のためにまた接続
   |<--- 接続確立 --------|
   |---- GET /logo.gif --->|
   |<--- GIF 返却 --------|
   |---- TCP 切断 ------->|

   |---- TCP 接続要求 ---->|  ← さらに別の画像のために
   ...

1990年代のページは画像10枚程度だった。それでも10回 TCP の3-way handshake をやることになる。1回の handshake で往復100ミリ秒として、それだけで1秒以上のロスだ。

↓ HTTP/1.0 のリクエスト毎の接続確立・切断 ↓

sequenceDiagram
    participant B as ブラウザ
    participant S as サーバー
    Note over B,S: 1個目: HTMLの取得
    B->>S: TCP SYN
    S->>B: SYN-ACK
    B->>S: ACK
    B->>S: GET /page.html
    S->>B: HTML応答
    B->>S: TCP FIN
    S->>B: FIN-ACK
    Note over B,S: 2個目: 画像の取得 (また接続から)
    B->>S: TCP SYN
    S->>B: SYN-ACK
    B->>S: ACK
    B->>S: GET /logo.gif
    S->>B: GIF応答
    B->>S: TCP FIN
    Note over B,S: 3個目: CSSの取得 (またまた接続から)
    B->>S: TCP SYN ...

TCP 接続のコストを軽視できなかった理由

1996年当時のインターネット回線は ISDN や 56kbps モデムが主流。1接続のセットアップ自体が体感速度に直結した。

現代の光回線で考えると、handshake 数十ミリ秒くらい大したことないと思うかもしれない。しかしモバイル環境(4G/5G)や海外サーバーへの接続では往復遅延が100ms 以上になることもザラで、現代でも TCP セットアップの回数は性能の致命傷になりうる。HTTP/2、HTTP/3 はここを潰しに行く設計でもある。


第3章: 全部入り - HTTP/1.1(1997)

仮想ホスト革命

1997年に登場した HTTP/1.1 が解決したものは多い。だが最も歴史的に重要な変更は、地味な1ヘッダの必須化だった。

Host: www.example.com

Host ヘッダ。これがあるかないかで、Web の経済構造が変わった。

なぜ Host ヘッダが革命なのか

HTTP/1.0 までは、1つの IP アドレス = 1つのサイトだった。つまり「example.com を運営する」には専用 IP アドレスを買い、専用サーバーを置く必要があった。

しかし IPv4 アドレスは43億個しかなく、Web サイトは1996年時点で既に10万を超えていた。「全 Web サイトが専用 IP を持つ」など不可能だ。

HTTP/1.1 で Host ヘッダが必須になったことで、1つのサーバー(1つの IP)が複数のドメインを使い分けられるようになった。これを 仮想ホスト(Virtual Host)と呼ぶ。

同じ IP 203.0.113.1 のサーバーが
  www.example.com への要求 → A サイトを返す
  www.shop.jp への要求       → B サイトを返す
  blog.takato.dev への要求   → C サイトを返す

これが 共有ホスティングクラウド時代 の前提条件になった。さくらインターネットも、Vercel も、Cloudflare も、Host ヘッダの存在を前提に成り立っている。

keep-alive で接続を使い回す

HTTP/1.1 のもう1つの大改良が 持続的接続(persistent connection、通称 keep-alive)だった。

[ブラウザ]                              [サーバー]
   |---- TCP 接続要求 ---->|
   |<--- 接続確立 --------|
   |---- GET /page.html ->|
   |<--- HTML 返却 -------|
   |---- GET /logo.gif --->|  ← 同じ接続を使い回す
   |<--- GIF 返却 --------|
   |---- GET /style.css -->|
   |<--- CSS 返却 --------|
   ...(一定時間後に切断)

1.0 時代に必須だった「リクエストごとの TCP セットアップ」が不要になった。これでページロードが劇的に速くなる。

↓ HTTP/1.1 の keep-alive と パイプライニングの違い ↓

sequenceDiagram
    participant B as ブラウザ
    participant S as サーバー
    Note over B,S: keep-alive (直列・応答待ち)
    B->>S: GET /page.html
    S->>B: HTML応答
    B->>S: GET /style.css
    S->>B: CSS応答
    B->>S: GET /logo.png
    S->>B: PNG応答
    Note over B,S: パイプライニング (まとめ送り・しかしHOLブロッキング)
    B->>S: GET /a.html
    B->>S: GET /b.css
    B->>S: GET /c.png
    Note right of S: aが遅いと b,cも詰まる
    S->>B: a応答 (遅い)
    S->>B: b応答 (待たされる)
    S->>B: c応答 (待たされる)

Connection: keep-alive ヘッダ

HTTP/1.1 ではデフォルトで keep-alive が有効。明示的に切りたい時だけ Connection: close を送る。

サーバー側にはタイムアウト設定がある(nginx の keepalive_timeout 65; など)。「最後の通信から65秒経ったら切る」のような運用。これは インフラ章 で詳しく扱う。

パイプライニング - 良い理想と悲しい現実

HTTP/1.1 にはさらに先進的な機能が用意されていた。パイプライニング(pipelining)だ。

通常、HTTP は「1リクエスト送る → 応答を待つ → 次のリクエスト送る」と直列だ。パイプライニングは「応答を待たずに次々送る」を可能にする。

普通:        [REQ1] → 待ち → [RES1] → [REQ2] → 待ち → [RES2]
パイプライン: [REQ1][REQ2][REQ3] → [RES1][RES2][RES3]

理論上、これで往復遅延がほぼゼロになる。

パイプライニングが普及しなかった理由

美しい設計だったが、現実は厳しかった。

  1. HOL ブロッキング(Head-of-Line blocking): パイプライン内の最初のリクエストが遅いと、後ろが全部詰まる。1個の遅いリクエストが残りの全レスポンスを止める
  2. プロキシ・サーバーのバグ: 当時の中間サーバーや古い実装がパイプライン処理を正しく扱えず、応答が壊れる事故が頻発
  3. デバッグ困難: 何が原因で詰まったか分かりにくい

結果、ブラウザは Firefox を除いてほぼ全社が「デフォルト無効」にした。標準仕様にあっても、現実に使われなければ意味がないという教訓。

この失敗が、HTTP/2 の 本物の多重化 につながる。

Chunked Transfer Encoding

もう1つの重要な拡張が チャンク転送エンコーディング(Chunked Transfer-Encoding)だった。

HTTP/1.0 では、応答のサイズを Content-Length ヘッダで前もって伝える必要があった。だが「サイズが事前に分からない」場面がある。たとえばデータベースからストリーミングで取り出すレスポンスや、リアルタイム生成される動画など。

HTTP/1.1 200 OK
Transfer-Encoding: chunked

7\r\n
Mozilla\r\n
11\r\n
 Developer Net\r\n
0\r\n
\r\n

チャンクごとに「これから何バイト送る」を宣言する形式。0 を送れば終了。

現代に残る重要性

Server-Sent Events(SSE)も、生成 AI のストリーミング応答(ChatGPT がじわじわ文字を出すアレ)も、内部的にはこの chunked transfer の上に乗っている。HTTP/1.1 の遺産が、LLM 時代まで生き残っている


第4章: バイナリの時代 - HTTP/2(2015)

Google が痺れを切らした

2009年、Google が痺れを切らした。HTTP/1.1 は10年以上使われているのに、本質的な性能改善が無い。彼らは独自プロトコル SPDY(スピーディと読む)を発表する。

SPDY の特徴:

  • バイナリプロトコル化(テキストではなくバイト列)
  • 1接続上で複数のストリームを並列に流す(本物の多重化)
  • ヘッダ圧縮
  • サーバー側からのプッシュ

Chrome に SPDY を実装し、Google のサーバーで動かして実証。「これで速くなるよね」と業界に提示した。IETF(インターネット技術の標準化団体)はこの実装をベースに HTTP/2 を策定する。2015年、RFC 7540 として公開された。

バイナリ化の意味

HTTP/1.1 までは人間が読めるテキストプロトコルだった。telnet www.example.com 80GET / HTTP/1.1 と打てば動く。これは デバッグしやすい という大きな利点があった。

HTTP/2 はこれを捨てた。バイナリフレーム形式に。

HTTP/1.1 (テキスト):
GET /index.html HTTP/1.1\r\n
Host: example.com\r\n
\r\n

HTTP/2 (バイナリ、概念図):
[HEADERS フレーム: stream=1, flags=END_HEADERS, ...]
[DATA フレーム: stream=1, payload=...]

なぜバイナリにしたか

  1. パースが速い(長さがビット位置で決まっている、正規表現も改行検出も不要)
  2. 誤解釈の余地が減る(テキストだと「改行は LF か CRLF か」「ヘッダ名は大文字小文字どっち」など曖昧な箇所が多く、実装によって差異が出てバグの温床になっていた)
  3. 状態管理がしやすい(ストリームの ID やフラグをビット単位で管理)

代償としてデバッグは難しくなった。tcpdump で見ても人間には読めない。代わりに Wireshark や Chrome DevTools の専用ビューアが必要。

ストリームの多重化 - 本物の並列化

HTTP/2 の核は ストリーム(stream)という概念だ。1つの TCP 接続上に、複数の論理的な「ストリーム」が同時に流れる。

1つの TCP 接続
   ├── Stream 1: /index.html のリクエストとレスポンス
   ├── Stream 3: /style.css のリクエストとレスポンス
   ├── Stream 5: /script.js のリクエストとレスポンス
   └── Stream 7: /logo.png のリクエストとレスポンス
       (全部が同時に流れる、順番を待たなくていい)

HTTP/1.1 のパイプラインと違って、ストリームは独立している。Stream 3 が遅くても Stream 5 は影響を受けない。

↓ HTTP/2 のストリーム多重化(1接続上で並列・独立) ↓

flowchart LR
    B[ブラウザ] -->|1つのTCP接続| MUX[多重化レイヤー]
    MUX -.Stream 1.-> S1[HTML]
    MUX -.Stream 3.-> S3[CSS]
    MUX -.Stream 5.-> S5[JS]
    MUX -.Stream 7.-> S7[画像1]
    MUX -.Stream 9.-> S9[画像2]
    S1 --> SRV[サーバー]
    S3 --> SRV
    S5 --> SRV
    S7 --> SRV
    S9 --> SRV
    Note1[各ストリームは独立<br/>1つが遅くても他に影響しない]

HTTP/1.1 時代のブラウザの苦闘

HTTP/1.1 では「1ドメインあたり同時接続は6つまで」というブラウザ側の制限があった(仕様ではなく Chrome、Firefox、Safari がそれぞれ決めた数字)。並列に画像を取りに行きたければ、ブラウザは6本の TCP 接続を別々に張る必要があった。

サイトを高速化するために、開発者は ドメインシャーディング(CSS/画像を static1.example.com static2.example.com のように別ドメインに分けて、ブラウザに「別サイト扱い」させて並列接続数を増やす)という小細工をしていた。

HTTP/2 ではドメインシャーディングは逆効果になる。1つの接続で全部多重化できるのだから、わざわざ複数接続を張る理由がなくなった。昨日のベストプラクティスが今日のアンチパターンになる典型例。

HPACK ヘッダ圧縮

Web ページを開くと、ブラウザは数十〜数百のリソース(HTML、CSS、JS、画像、フォント)を取りに行く。それぞれの HTTP リクエストには大量のヘッダが付く。

GET /image.png HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ...
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br
Cookie: session_id=abc...; tracking_id=xyz...; ...(数 KB)

これがリクエストごとに送られる。500バイト × 100リクエスト = 50KB の冗長データ。

HTTP/2 は HPACK という独自圧縮を導入した。「以前送ったヘッダはインデックス番号で参照する」「よく使うヘッダ名は静的テーブルに事前定義」など、HTTP に特化した圧縮アルゴリズム。

なぜ汎用圧縮(gzip など)を使わなかったか

実は HTTP/2 の前身 SPDY では gzip でヘッダ圧縮していた。だが CRIME 攻撃(2012)でこれが脆弱だと判明した。汎用圧縮は「平文の中に攻撃者制御の文字列を混ぜると、圧縮率の変化からシークレットを推測できる」という性質があった。

HPACK はこの教訓を踏まえ、「圧縮による情報漏洩を防ぐ」設計になっている。性能だけでなくセキュリティも考慮した圧縮。プロトコル設計の難しさを物語る一件。

Server Push - 良い意図、悲しい結末

HTTP/2 には Server Push(サーバープッシュ)という野心的な機能があった。

普通の HTTP は「クライアントが要求 → サーバーが応答」だ。Server Push は「サーバーが、クライアントが要求する前に、関連リソースを先回りで送る」機能だった。

ブラウザ: GET /index.html
サーバー: ここに HTML、ついでに style.css と script.js も送っとくね
(ブラウザが HTML をパースして CSS を要求する前に、もう CSS が届いている)

理論上、ページロードがさらに速くなる。

Server Push が廃止された顛末

美しい機能だったが、現実は厳しかった。

  1. キャッシュの問題: ブラウザが既に持っている CSS を、サーバーは知らずに送り直してしまう。帯域の無駄
  2. 実装の複雑さ: 何を push すべきか、サーバー側で判断するロジックが難しい
  3. 同等の効果は別の手段で得られる: <link rel="preload"> という HTML ヒントで、ブラウザに「これも先に取っといて」と教える方が制御しやすい

2020年、Chrome が Server Push サポートを停止すると発表。2022年、HTTP/3 仕様では「実質非推奨」扱いに。

標準仕様にあっても、現実に勝てなければ消える。これは HTTP/1.1 のパイプライニングと同じパターン。プロトコル設計は理論だけでは決まらない。


第5章: TCP を捨てた - HTTP/3 と QUIC(2022)

HTTP/2 でも残った問題

HTTP/2 で多重化を実現したが、まだ問題があった。TCP レイヤーの HOL ブロッキングだ。

HTTP/2 が「ストリームを多重化」していると言っても、
それは HTTP のレイヤーの話。
下の TCP は「順番通りにパケットを届ける」プロトコル。

ストリーム1のパケットが1つ落ちると、
たとえストリーム2やストリーム3のパケットが既に届いていても、
TCP は「順番を保つため」全部の処理を止める。

つまり、HTTP/2 はアプリ層で多重化したが、TCP がそれを台無しにしていた。

TCP は変えられない

「じゃあ TCP を改良すればいい」と思うかもしれない。だが TCP は世界中のルーター、ファイアウォール、OS、NAT ボックスに実装されている。それを全部更新するのは不可能だ。

プロトコルの硬直化(Protocol Ossification)

インターネットの中間機器(特に企業ファイアウォール)は「自分が知らない TCP オプション」を見ると「攻撃かもしれない」と判断してパケットを落とすことがある。これによって TCP に新しい機能を追加しようとしても、実際には中間機器で阻まれて動かない

これを プロトコルの硬直化と呼ぶ。Tim Berners-Lee の時代のシンプルさはとうに失われ、今やインターネットの足回りは「動いてるけど変えられない」状態。

Google はこの状況に対する解として、TCP の外側で新プロトコルを作ることを選んだ。

QUIC - UDP の上に新世界を築く

Google は2012年から QUIC(Quick UDP Internet Connections)の実験を始めた。

QUIC の基本思想:

  • 下回りには UDP を使う(中間機器が UDP を阻むことは少ない)
  • UDP の上に、独自に「信頼性のある順序保証通信」を再実装する
  • TLS 1.3 を統合する(暗号化が標準)
  • 接続単位ではなくストリーム単位の HOL ブロッキング解消
従来:
  HTTP/2 → TLS → TCP → IP → 物理層

QUIC:
  HTTP/3 → QUIC(中に TLS と信頼性を内包) → UDP → IP → 物理層

「TCP の機能を UDP の上で再実装した」とはどういうことか

UDP は「相手に届くか保証しない、順番も保証しない、再送もしない」シンプルなプロトコル。「データを投げるだけ」のレイヤー。

QUIC は UDP の上に以下を載せている:

  • 再送制御(届かなければ送り直す)
  • 順序制御(送った順に並び替える)
  • 輻輳制御(ネットワーク混雑時の流量調整)
  • 暗号化(TLS 1.3 内蔵)
  • ストリーム独立性(HTTP/2 のような多重化を、TCP の制約なしで実現)

つまり QUIC は「アプリ層で再発明された TCP + TLS」と言える。ただし TCP の制約から自由なので、HOL ブロッキングや手探りで決まったオプションの硬直化から解放されている。

HTTP/3 の利点 - モバイル時代の答え

HTTP/3 が一番威力を発揮するのは モバイル環境だ。

コネクションマイグレーション

スマホで Wi-Fi から 4G に切り替わると、IP アドレスが変わる。TCP は「送信元 IP + ポート + 宛先 IP + ポート」で接続を識別するので、IP が変わると 接続が切れる。動画ストリーミングが止まり、ダウンロードが中断される。

QUIC は Connection ID(接続 ID)で接続を識別する。IP が変わっても同じ接続 ID を使い続ければ、接続は維持される。

これにより、移動しながらの動画視聴や、地下鉄で電波が途切れがちな環境でもセッションが切れない。スマホ時代の通信プロトコルとして設計されている。

↓ HTTP/2 (TCP上) と HTTP/3 (QUIC/UDP上) の比較 ↓

flowchart TB
    subgraph H2[HTTP/2 + TCP]
        direction TB
        A1[HTTP/2: ストリーム多重化]
        A2[TLS 1.2/1.3]
        A3[TCP: 順序保証で全体が止まる<br/>HOLブロッキング]
        A4[IP]
        A1 --> A2 --> A3 --> A4
    end
    subgraph H3[HTTP/3 + QUIC]
        direction TB
        B1[HTTP/3]
        B2[QUIC: TLS 1.3内蔵<br/>ストリーム単位で独立<br/>Connection IDで接続維持]
        B3[UDP: 配送するだけ]
        B4[IP]
        B1 --> B2 --> B3 --> B4
    end

普及の現状

2022年、RFC 9114 として HTTP/3 が標準化された。

  • CloudflareGoogleMeta(Facebook) は早期から本番投入
  • Chrome、Firefox、Safari すべて対応済み
  • 2024年時点で、Web トラフィックの30%程度が HTTP/3 を経由(増加傾向)

バックエンドエンジニアにとっての HTTP/3

HTTP/3 を「アプリ側で意識する」場面はほぼない。あなたが Go や Node.js でサーバーを書いても、HTTP/3 化するのは前段のリバースプロキシ(nginx、Caddy、Cloudflare)の仕事。

ただし「なぜ Cloudflare 経由だと体感速度が違うのか」「モバイルでセッションが切れない理由」を 語れることは、インフラ周りの議論で説得力を持つ。これが教養としての価値。


第6章: ステータスコードの設計思想

3桁の数字に込められた意味

HTTP のステータスコードは、ただの番号ではない。5つのカテゴリに思想がある

範囲カテゴリ意味
1xxInformational処理中、追加情報
2xxSuccess成功
3xxRedirection別の場所を見てくれ
4xxClient Errorあなた(クライアント)が悪い
5xxServer Errorこちら(サーバー)が悪い

↓ HTTP ステータスコードのカテゴリツリー ↓

flowchart TB
    Root[HTTPステータスコード] --> C1[1xx Informational<br/>処理中]
    Root --> C2[2xx Success<br/>成功]
    Root --> C3[3xx Redirection<br/>転送]
    Root --> C4[4xx Client Error<br/>クライアントが悪い]
    Root --> C5[5xx Server Error<br/>サーバーが悪い]
    C2 --> S200[200 OK]
    C2 --> S201[201 Created]
    C2 --> S204[204 No Content]
    C3 --> S301[301 Moved Permanently]
    C3 --> S302[302 Found]
    C3 --> S304[304 Not Modified]
    C4 --> S400[400 Bad Request]
    C4 --> S401[401 Unauthorized<br/>未認証]
    C4 --> S403[403 Forbidden<br/>権限なし]
    C4 --> S404[404 Not Found]
    C4 --> S429[429 Too Many Requests]
    C5 --> S500[500 Internal Server Error]
    C5 --> S502[502 Bad Gateway]
    C5 --> S503[503 Service Unavailable]
    C5 --> S504[504 Gateway Timeout]

なぜ「誰が悪いか」を分けるか

4xx と 5xx の分離は深い意味を持つ。監視・運用の観点で「アラートを出すべきか」が変わるから。

  • 4xx 多発: クライアント(ユーザー、フロントエンド、外部の bot)が変なリクエストを送っている → 監視は要るが、緊急ではない
  • 5xx 多発: サーバーが壊れている → 即時アラート、深夜でも叩き起こす

本番運用では 5xx > 1% のような閾値で PagerDuty を鳴らす。エンジニアが寝てる時に呼び出される根拠が、この1桁目の数字だ。

よく見る顔ぶれ

必ず覚えるステータスコード

コード名前意味と使い所
200OK普通の成功
201CreatedPOST でリソースを作った時。Location ヘッダに URL を入れる慣習
204No Content成功だが本文なし。DELETE の応答でよく使う
301Moved PermanentlyURL が恒久的に変わった、ブックマーク更新してくれ
302Found一時的なリダイレクト
304Not Modifiedキャッシュが有効、再ダウンロード不要
400Bad Requestリクエストが文法的におかしい
401Unauthorized認証が必要(実は「未認証」の意味、命名ミス)
403Forbidden認証はOKだが権限がない
404Not Foundリソースが存在しない
409Conflict状態が競合(楽観ロック失敗、リソース重複)
422Unprocessable Entity文法はOKだが内容が無理(バリデーションエラー)
429Too Many Requestsレート制限に引っかかった
500Internal Server Errorサーバー側の何かが壊れた、詳細不明
502Bad Gatewayプロキシ/ロードバランサが背後のサーバーから変な応答を受けた
503Service Unavailable一時的に提供できない(メンテ中、過負荷)
504Gateway Timeoutプロキシが背後のサーバーから応答を待ち切れず諦めた

401 と 403 の混乱

401 は「Unauthorized」だが意味は「Unauthenticated」

ステータスコードの命名で最も悪名高いのが 401。「Unauthorized」と書かれているが、実際の意味は 「認証していない(誰だか分からない)」

「認証はしたが権限がない」のは 403 Forbidden。

401: あなたが誰だか分からない → ログインしてくれ
403: あなたが誰かは分かったが、これはダメ → 諦めてくれ

命名を間違えたまま標準になり、今さら変えられない。仕様策定で1単語間違えると30年苦しむ例。

418 I’m a teapot - 仕様書のジョーク

エイプリルフールに作られた仕様

RFC 2324 (1998) は Hyper Text Coffee Pot Control Protocol という、Web 経由でコーヒーポットを制御するための仕様。これは完全にジョーク仕様。

その中で「ティーポットにコーヒーを淹れさせようとした時の応答」として 418 I'm a teapot が定義された。エイプリルフール仕様だが、なぜか Google、GitHub、Apple のサーバーで実装され、現代でも一部 API で使われている(特にスパム検出時のブロック応答として)。

インターネットには遊び心が残っている。


第7章: メソッドの本来の意味

GET と POST の本質的な違い

HTTP メソッドは初心者には「GET は読み、POST は書き」程度に教えられがちだが、本質は**「冪等性」(idempotency)**にある。

冪等性とは

「同じ操作を何回繰り返しても、結果が同じ」という性質。

数学的に言うと、関数 f が冪等なら f(f(x)) = f(x)

HTTP の文脈では、「同じリクエストを何回送っても、サーバーの状態が同じになる」が冪等。

メソッドごとの性質を整理する。

メソッド安全冪等用途
GETはいはいリソースの取得
HEADはいはいGET と同じだが本文なし
OPTIONSはいはいサポートメソッドの問い合わせ
PUTいいえはいリソースの完全な置き換え
DELETEいいえはいリソースの削除
POSTいいえいいえ新規作成、汎用処理
PATCHいいえいいえ部分更新(実装次第)

安全」とは「サーバー状態を変えない」、「冪等」とは「同じ呼び出しを繰り返しても結果が同じ」。

↓ HTTP メソッドの分類(安全性と冪等性) ↓

flowchart TB
    Root[HTTPメソッド] --> Safe[安全<br/>サーバー状態を変えない]
    Root --> NotSafe[安全でない<br/>サーバー状態を変える]
    Safe --> GET[GET<br/>取得・冪等]
    Safe --> HEAD[HEAD<br/>ヘッダのみ・冪等]
    Safe --> OPTIONS[OPTIONS<br/>問い合わせ・冪等]
    NotSafe --> Idemp[冪等<br/>何回叩いても同じ結果]
    NotSafe --> NotIdemp[冪等でない<br/>叩くたびに変わる]
    Idemp --> PUT[PUT<br/>完全置換]
    Idemp --> DELETE[DELETE<br/>削除]
    NotIdemp --> POST[POST<br/>新規作成・汎用]
    NotIdemp --> PATCH[PATCH<br/>部分更新]

なぜ冪等性が重要なのか

実務での冪等性の意味

ネットワークは不安定だ。リクエストを送ったが応答が来ない場合、「届いていないのか、応答だけ失われたのか」が分からない。

このとき、冪等なメソッドなら安全にリトライできる

GET /users/42       → 失敗 → リトライしても同じ結果なので安全
DELETE /users/42    → 失敗 → リトライしてもユーザーが消えているだけなので安全
POST /payments      → 失敗 → リトライしたら二重決済の危険性!

決済 API では POST が標準だが、二重決済を防ぐために 冪等性キー(Idempotency-Key ヘッダ)を導入する。Stripe API、PayPal API などはこれを必須にしている。

POST /v1/charges
Idempotency-Key: a3f9b2c1-...

サーバーは「同じキーで来た決済は1回しか実行しない」と保証する。これにより POST も実質冪等にできる。

PUT と PATCH の違い

PUT は全置換、PATCH は部分更新

現在のリソース:
  { "name": "Alice", "age": 30, "email": "alice@example.com" }

PUT /users/42
  { "name": "Alice" }
  → 結果: { "name": "Alice" }(age と email は消える)

PATCH /users/42
  { "name": "Alice" }
  → 結果: { "name": "Alice", "age": 30, "email": "alice@example.com" }
  (name だけ変更、他は維持)

実務では PATCH が圧倒的に使われる。「住所だけ変えたい」のに全フィールドを送る PUT は冗長すぎる。

しかし PATCH の挙動は仕様で曖昧。JSON Patch(RFC 6902)や JSON Merge Patch(RFC 7396)など複数の標準があり、実装次第。

DELETE は冪等

DELETE を2回叩いても怒られない理由

「もう消えてるのに DELETE 送ったら 404 じゃないの?」と思うかもしれない。

仕様上は OK だが、実務的には 2回目以降も 200 や 204 を返す API も多い。なぜなら「結果として消えている」状態は同じだから、冪等性の観点では成功扱いで良い。

「2回目で 404 を返す」設計は、クライアントに「リトライしたら失敗した」と勘違いさせる可能性があり、推奨されない。


1994年、Netscape での発明

HTTP は ステートレス(stateless)プロトコルだ。サーバーは「前のリクエストで誰が来たか」を覚えていない。

これは設計として美しい(サーバーが状態を持たないのでスケールしやすい)が、E コマースには困る。「カートに商品を入れる」「ログイン状態を保持する」が実現できない。

1994年、Netscape 社のエンジニア Lou Montulli が解決策を発明した。Cookie(クッキー)だ。

1回目: クライアント → サーバー: GET /
       サーバー → クライアント: Set-Cookie: session_id=abc123

2回目: クライアント → サーバー: GET / 
       Cookie: session_id=abc123
       (ブラウザが自動で前回受け取った値を送る)

サーバーは session_id から「これは誰か」を判別する。

第三者 Cookie の悪用

当初の Cookie は「同じドメインに送る」だけのシンプルな仕組みだった。だが広告業界はこれを悪用した。

example.com を訪問
  ページに含まれる広告画像が doubleclick.net から読み込まれる
  doubleclick.net がブラウザに Cookie をセット

別のサイト shop.jp を訪問
  ここにも doubleclick.net の広告が
  ブラウザは先ほどの Cookie を doubleclick.net に送る
  doubleclick.net は「同じ人が example.com と shop.jp を見た」と分かる

これを 第三者 Cookie(third-party cookie)と呼ぶ。ユーザーの全 Web 行動を1社が追跡できる仕組み。

2020年代、Safari と Firefox は第三者 Cookie をデフォルトで無効化。Chrome も2024年から段階的に廃止を進めている(が、何度も延期されている)。プライバシーと広告ビジネスの戦争 が続いている。

SameSite 属性 - CSRF からの防衛

SameSite 属性

2016年頃から導入された Cookie 属性。「他のサイトからのリクエストにこの Cookie を送るか」を制御する。

Set-Cookie: session_id=abc; SameSite=Strict
  → 別サイトからは絶対に送らない、最も安全

Set-Cookie: session_id=abc; SameSite=Lax
  → 通常のリンククリックでは送る、フォーム POST では送らない

Set-Cookie: session_id=abc; SameSite=None; Secure
  → 第三者にも送る、ただし HTTPS 必須

CSRF(Cross-Site Request Forgery)という攻撃を防ぐための基本機能。詳しくは セキュリティ章 で扱う。

2020年、Chrome がデフォルトを Lax に変更。それまで何も指定しなくても「全サイトに送る」だったのが、明示しないと安全側に倒れる仕様に。インターネット全体の挙動が一夜で変わる瞬間だった。


第9章: REST、GraphQL、tRPC - API スタイルの流転

REST の登場(2000)

2000年、Roy Fielding(HTTP/1.1 仕様の主要著者の1人)が博士論文で REST(REpresentational State Transfer)を提唱した。

REST の本質は「HTTP の本来の使い方を再発見しよう」だった。

リソース指向:
  /users          → ユーザー一覧
  /users/42       → ユーザー42人目
  /users/42/posts → ユーザー42人目の投稿一覧

メソッドで動詞:
  GET /users/42      → 取得
  POST /users        → 作成
  PUT /users/42      → 更新
  DELETE /users/42   → 削除

これまでの SOAP や XML-RPC は「POST /api/doStuff」のように全部 POST で叩く設計だった。REST は「HTTP メソッドとパスで意味を表現する」アプローチで、シンプルさを取り戻した。

REST の限界

REST の課題

2010年代後半、REST の限界が露呈し始めた。

  1. オーバーフェッチ: 必要のないフィールドまで返ってくる(ユーザー名だけ欲しいのに全プロフィールが返る)
  2. アンダーフェッチ: 必要な情報を集めるのに複数 API を叩く必要がある(N+1 問題)
  3. モバイル不利: 帯域とリクエスト数が増えると、電池とデータ通信料を消費
  4. エンドポイント爆発: 「ユーザーの最新投稿の最新コメントの返信を取りたい」のような複雑な要求にエンドポイントを切るのが大変

GraphQL(2015)

Facebook がモバイルアプリ開発で REST に困っていた。彼らの解が GraphQL だ。

query {
  user(id: 42) {
    name
    posts(limit: 5) {
      title
      comments(limit: 3) {
        text
      }
    }
  }
}

クライアントが「欲しいフィールドを宣言する」スタイル。1回のリクエストで必要なものだけ取れる。

GraphQL の利点と課題

利点:

  • 1リクエストで複数階層のデータが取れる
  • 不要なフィールドが転送されない
  • 強力な型システム(スキーマで全 API が定義される)

課題:

  • サーバー側の実装が複雑(任意のクエリに応える必要がある)
  • N+1 問題が逆に深刻化することも(実装次第で)
  • キャッシュが難しい(URL ベースの HTTP キャッシュが効かない)
  • 認可制御が複雑(フィールド単位で権限管理)

tRPC への揺り戻し(2020年代)

GraphQL が複雑すぎる、と感じる開発者が現れた。TypeScript モノレポ環境では、「フロントとバックで同じ言語を使うなら、もっとシンプルにできるはず」という発想で tRPC が生まれた。

// サーバー側
const appRouter = router({
  getUser: publicProcedure
    .input(z.object({ id: z.number() }))
    .query(({ input }) => db.users.find(input.id)),
});
 
// クライアント側
const user = await trpc.getUser.query({ id: 42 });
// 型は自動推論される、IDE 補完が完璧

なぜ「揺り戻し」が起きたか

技術の進化は直線的ではない。

SOAP(複雑、XML)
  → REST(シンプル、HTTP 本来の使い方)
    → GraphQL(クエリで柔軟性)
      → tRPC(型安全、シンプル)

どれも「前世代の課題」を解決して登場するが、新世代も別の課題を作る。「銀の弾丸はない」(Fred Brooks)の典型。

重要なのは「それぞれの選択が、どの課題を解決し、どの課題を抱えているか」を理解すること。tRPC は TS 専用、GraphQL は他言語フロント・複雑な BFF に向く、REST は公開 API・キャッシュ要件・互換性に向く。仕事に応じて選ぶ


第10章: 1リクエストの旅 - URL を入力してから画面が出るまで

ブラウザのアドレスバーで Enter を押した瞬間、何が起きるか

URL https://www.example.com/ を入力して Enter を押す。0.5秒後にページが表示される。この間に世界中で何が起きているか、追ってみよう。

ステップ 1-3: DNS → TCP → TLS

DNS 解決:
  ブラウザ → OS → ISP DNS → ルート → .com DNS → 権威 DNS
  www.example.com → 203.0.113.42

TCP 3-way handshake:
  ブラウザ → サーバー: SYN
  サーバー → ブラウザ: SYN-ACK
  ブラウザ → サーバー: ACK

TLS handshake:
  Client Hello → Server Hello + 証明書 → 鍵交換 → 暗号化開始

詳細は 03. DNS04. TLS で扱う。

↓ URL を開いてからページが表示されるまでの全工程 ↓

sequenceDiagram
    participant U as ユーザー
    participant B as ブラウザ
    participant D as DNS
    participant S as サーバー
    U->>B: URL入力 + Enter
    Note over B,D: 1. DNS解決
    B->>D: www.example.com を問い合わせ
    D->>B: 203.0.113.42
    Note over B,S: 2. TCP接続 (3-way handshake)
    B->>S: SYN
    S->>B: SYN-ACK
    B->>S: ACK
    Note over B,S: 3. TLSハンドシェイク
    B->>S: Client Hello
    S->>B: Server Hello + 証明書
    B->>S: 鍵交換
    S->>B: 暗号化セッション開始
    Note over B,S: 4. HTTPリクエスト/レスポンス
    B->>S: GET / HTTP/2 + Cookie
    S->>B: 200 OK + HTML
    Note over B: 5. レンダリング
    B->>S: GET /style.css
    B->>S: GET /script.js
    B->>S: GET /image.png
    S->>B: 並列でリソース返却
    B->>U: ページ表示

ステップ 4: HTTP リクエスト

GET / HTTP/2
Host: www.example.com
User-Agent: Mozilla/5.0 ...
Cookie: session_id=abc...

ステップ 5: サーバー側の処理

CDN(Cloudflare 等): キャッシュにあれば即返却
  ↓ なければ
ロードバランサ(nginx, ALB): バックエンドへ振り分け
  ↓
アプリサーバ(Go, Node.js):
  ルーティング → 認証 → DB 問い合わせ → HTML 生成 → 応答

ステップ 6-7: レスポンスとレンダリング

HTTP/2 200 OK + HTML が返却

ブラウザ:
  HTML パース → CSS/JS/画像を並列取得
  DOM + CSSOM → Render Tree → Layout → Paint → Composite

そして画面に Web ページが現れる。

DevTools の Network タブで観察できる

Chrome や Firefox の DevTools を開き、Network タブを見ながらサイトを開いてみると、上記のすべての段階が時系列で見える。

  • DNS Lookup
  • Initial Connection(TCP)
  • SSL(TLS handshake)
  • Request sent
  • Waiting for response(サーバー処理時間 = TTFB)
  • Content Download

これがリクエストの「旅」を可視化したもの。バックエンドエンジニアは Network タブを息するように読めるべき。性能問題の8割はここで見える。

1リクエストに関わる技術スタック

  • DNS: ドメイン名から IP アドレスへの変換 → 03
  • TCP / QUIC: 信頼性のあるバイト列伝送 → 01
  • TLS: 暗号化と認証 → 04
  • HTTP: アプリケーション層のリクエスト/レスポンス(この記事)
  • HTML/CSS/JS: ブラウザでの描画とインタラクション
  • ロードバランサ・CDN: スケーラビリティと配信最適化 → インフラ章
  • アプリケーションサーバ: ビジネスロジック実行
  • データベース: 永続化 → 06

たった1つの URL クリックに、これだけの技術が連動している。バックエンドエンジニアは、この全体図を頭に描けるかで評価が変わる。


章の問い (考えるための小さなトリガー)

読みながら / 読み終わって考えてほしいこと

  1. HTTP/1.1 のパイプライニングと HTTP/2 の多重化は「並列にリクエストを送る」という点では似ている。なぜ片方は失敗し、片方は成功したのか?
  2. Server Push が廃止された一方で <link rel="preload"> が生き残った。この差は、技術的優劣? それともプロトコル設計の哲学の差?
  3. あなたが今作っている API は REST / GraphQL / tRPC のどれが向く? 「なんとなく REST」で選んでないか?

おわりに - 過去30年と、これから

HTTP/0.9 の1行プロトコルから、HTTP/3 の QUIC まで、30年で大きく姿を変えた。だが**「URL でリソースを識別し、メソッドで操作を表現する」**という Tim Berners-Lee の根本思想は、今も生きている。

世代を超えて変わらないのは、設計思想の良し悪しではない。「単純で、拡張可能で、人間が理解できる」ものが長く生き残る、という事実だ。

バックエンドエンジニアにとっての教訓

  1. 単純さは武器: HTTP/0.9 の素朴さが普及の鍵だった。あなたの API も、無駄に複雑にしない
  2. 後方互換性は宝: HTTP/1.1 のリクエストは今でも動く。動いているものは壊さない
  3. 標準仕様も完璧ではない: パイプライニングも Server Push も失敗した。「仕様にあるから使う」ではなく「現実に使われているか」を見る
  4. 歴史を知ると今が見える: なぜ Cookie が嫌われるか、なぜ HTTP/3 が UDP の上に立つかは、歴史を知らないと分からない

プロトコル設計には、技術的な美しさだけでなく、政治・経済・運用の現実がのし掛かる。それを乗り越えた仕様だけが、生き残る。

30年後のあなたへ

この記事を読んでいるあなたが、もし2056年もエンジニアを続けていたら、HTTP/4 や HTTP/5 が当たり前になっているかもしれない。あるいは HTTP 自体が別のプロトコルに置き換わっているかもしれない。

それでも変わらないのは、**「リソースを識別し、操作する」**というインターネットの根源的な発想だろう。Tim Berners-Lee が CERN の地下で考えたその思想は、おそらくこれからも生き続ける。


次の読み物

DNS の仕組みも、HTTP と並んで「ブラウザのアドレスバーから始まる旅」の主役だ。www.example.com という文字列が、どうやって IP アドレス 203.0.113.42 に変わるのか。世界中に分散した DNS サーバーの協調動作を覗いてみる。

03. DNS の仕組み - インターネットの電話帳

または、HTTP の暗号化を支える TLS の中身に踏み込みたければ:

04. 暗号と TLS - HTTPS の中身


参考: もっと深く知りたい人へ

一次資料

  • RFC 1945 - HTTP/1.0(1996)
  • RFC 7230-7235 - HTTP/1.1 改訂版(2014)
  • RFC 7540 - HTTP/2(2015)
  • RFC 9114 - HTTP/3(2022)
  • RFC 9000 - QUIC(2021)

書籍

  • Real World HTTP』渋川よしき著 - 日本語で HTTP の全体像を網羅した名著
  • ハイパフォーマンス ブラウザネットワーキング』Ilya Grigorik 著 - HTTP/2、TLS、TCP の性能特性を徹底解説

OSS

  • curl - HTTP の生実装、勉強用に最高
  • Caddy - Go 製、HTTP/3 対応 Web サーバ、ソースが読みやすい
  • quiche - Cloudflare の QUIC 実装

ブラウザ DevTools

  • Chrome DevTools の Network パネルに1日30分触る
  • chrome://net-export/ で詳細ログ取得
  • 1リクエストの「旅」を毎日観察する