FlowingDev

HTTP: ウェブを動かす\"ハガキ\"たち

ウェブを支える基本のテキストメッセージ、生のHTTPリクエストとレスポンスの仕組みを学ぼう。ヘッダー、ボディ、ステータスラインからなるその構造を解き明かします。

ツールを試す: HTTP メッセージビューア

一言でいうと

HTTPメッセージとは、クライアント(君のブラウザとか)とサーバーが、ウェブページやデータ、そしてネット中にあふれる猫の画像をリクエストしたり送信したりするために使う、特別なフォーマットのプレーンテキストの塊のことだ。

解決する問題

デジタルの石器時代(80年代後半から90年代初頭)のインターネットは、ちょっとした無法地帯だった。ファイル転送にはFTP、文書メニューにはGopher、その他にもニッチなシステムがたくさんあって、それぞれに違うプロトコルがあったんだ。奴らは互いにうまく話せなかった。まるで、連絡を取りたい相手ごとに、違う種類の郵便配達員と封筒が必要になるようなものだった。

そこへ、ティム・バーナーズ=リー卿が「World Wide Web」という、リンクで結ばれたハイパーテキスト文書の統一システムというビジョンを掲げて現れた。これを実現するために、彼はどんなコンピュータでも文書を要求し、それを受け取ることができる、シンプルで普遍的な言語を必要としていた。それはステートレスである必要があった。つまり、各リクエストが自己完結したイベントであり、サーバーが過去の会話を覚えておく必要がないということ。そして何より重要なのは、デバッグを簡単にするために、少なくとも原理的には人間が読める(ヒューマンリーダブルな)ものである必要があった。

そこで登場したのが、Hypertext Transfer Protocol、略してHTTPだ。HTTPは、標準的なメッセージフォーマット、つまりウェブのための世界共通の「ハガキ」を定義することで、この問題を解決した。このハガキには、宛先(サーバーとパス)、差出人の情報、中身についての簡単なメモ(ヘッダー)、そして実際のコンテンツ(ボディ)を書き込むための指定された場所がある。この標準化された構造のおかげで、どんなクライアントもどんなサーバーとも通信できるようになり、私たちが今日知っている、相互運用可能なウェブが生まれたんだ。

内部の仕組み

HTTPメッセージの核心は、ただのテキストストリームだ。しかし、ただのテキストではない。厳格な構造を持っている。デジタルのナプキンに「ホームページよこせ!」って走り書きしてサーバーに投げつける、なんてことはできない。メッセージは主に2つの種類に分かれている。リクエスト(要求)とレスポンス(応答)だ。

リクエストメッセージの解剖学

これは、君がURLを入力したりリンクをクリックしたりしたときに、ブラウザが送信するものだ。最大3つのパートから構成され、特定の改行コード(CRLF、コードで書くと\r\n)で区切られている。

  1. スタートライン (Start-Line): 何を、どこから欲しいのか、そしてどの言語バージョンで話しているのかを伝える一行。 METHOD /path/to/resource HTTP/Version

    GET /documentation/guides/http HTTP/1.1
    
    • GET は メソッド。リクエストの「動詞」にあたる。
    • /documentation/guides/http は リソースパス。
    • HTTP/1.1 は プロトコルバージョン。
    一般的なメソッド 意味 ボディを持つか?
    GET 「このリソースをください」 いいえ
    POST 「このデータで何か新しいものを作って」 はい
    PUT 「このデータで更新/置換して」 はい
    DELETE 「このリソースを削除してください」 いいえ
    HEAD 「ヘッダーだけちょうだい、ボディはいらない」 いいえ
  2. ヘッダー (Headers): リクエストに関するメタデータを提供する Key: Value のペアの集まり。ハガキの裏にあるチェックボックスやメモ書きみたいなものだと考えてほしい。

    Host: flowing.dev
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Language: en-US,en;q=0.5
    
    • Host: 最も重要なヘッダー。君がどのウェブサイトにアクセスしようとしているかをサーバーに伝える。1つのIPアドレスで複数のサイトをホストしているサーバーには不可欠だ。
    • User-Agent: 「私が使っているブラウザ/ツールはこれです」
    • Accept: 「できれば、これらのフォーマットでコンテンツを受け取りたいです」
  3. 超重要な空行: 最後のヘッダーの後には、完全に空っぽの行(CRLF)が1つだけ入る。これは絶対に譲れない区切り文字だ。「ヘッダーはここまで、次はボディ(もしあれば)が始まるよ」という合図になる。

  4. ボディ (Body) (任意): ペイロード。GETやHEADリクエストでは空っぽ。POSTやPUTの場合は、APIコールで送るJSONペイロードやフォームの入力内容など、送信するデータがここに入る。

    {
      "username": "dev-guru",
      "email": "guru@example.com"
    }
    

レスポンスメッセージの解剖学

これはサーバーが送り返してくるものだ。リクエストの構造を反映しているが、役割は異なる。

  1. ステータスライン (Status-Line): リクエストが成功したかどうか、そしてその理由を伝える一行。 HTTP/Version StatusCode StatusText

    HTTP/1.1 200 OK
    
    • StatusCode が最も重要な部分だ。結果を要約する3桁の数字。
    コード系統 意味 例
    2xx 成功!すべてうまくいった。 200 OK
    3xx リダイレクト。よそを見てくれ。 301 Moved Permanently
    4xx クライアントエラー。君のせいだよ。 404 Not Found
    5xx サーバーエラー。こっちのせいだよ。 500 Internal Server Error
  2. ヘッダー (Headers): レスポンスについて記述するKey: Valueペア。

    Date: Fri, 24 May 2024 12:00:00 GMT
    Content-Type: text/html; charset=utf-8
    Content-Length: 4096
    Cache-Control: max-age=600
    
    • Content-Type: 「私が送っているのはこれです。この場合は、UTF-8でエンコードされたHTMLドキュメントです」
    • Content-Length: 「私のレスポンスのボディは、正確に4096バイトです」
    • Cache-Control: 「君(または中継するプロキシ)は、このコピーを600秒間保存していいよ」
  3. 空行: こちらにも存在する。ヘッダーとボディを分ける。

  4. ボディ (Body): 君が要求した、まさにそのモノ!ウェブページのHTML、APIからのJSONデータ、画像ファイルなど。これがContent-Typeで言うところの「コンテンツ」だ。

実世界での話

謎の401事件

ある開発者がサードパーティAPIとの連携を担当していた。彼女は正しいAPIキーを送っていると確信していたが、すべてのリクエストが401 Unauthorizedエラーで返ってきた。彼女のコードは完璧に見えた: api.setAuth('my-secret-key')。イライラした彼女は、自分のフレームワークが送信している生のHTTPリクエストをキャプチャしてみた。

生のメッセージが真実を明らかにした:

POST /v1/widgets HTTP/1.1
Host: api.thirdparty.com
Content-Type: application/json
Api-Key: my-secret-key

{ "name": "New Widget" }

彼女はAPIのドキュメントをもう一度読み返した。認証ヘッダーはApi-KeyではなくAuthorizationであるべきで、その値はBearer で始まる必要があった。彼女が使っていたフレームワークの抽象化は単純すぎて、間違ったヘッダー名を使っていたのだ。彼女はヘルパーメソッドを迂回し、手動でヘッダーを設定したところ、次のリクエストは201 Createdで見事成功した。

教訓: フレームワークやライブラリは便利な抽象化だが、生のHTTPメッセージが絶対的な真実だ。何かがおかしいと感じたら、実際にネットワークで何が送られているのか、生のメッセージを調べること。

キャッシュの難問

マーケティングチームが新しいランディングページを公開したが、社内の半数の人が、必死にCtrl+F5を連打しても、古い「近日公開」ページしか表示されないでいた。開発者は、サーバー側のコードの問題ではないと主張した。キャッシュの問題を疑った彼は、ツールを使ってそのページの生のHTTPレスポンスヘッダーを調査した。

サーバーからのレスポンスはこんな感じだった:

HTTP/1.1 200 OK
Content-Type: text/html
...
Cache-Control: public, max-age=86400
Age: 34500

Cache-Controlヘッダーが、経路上にあるすべてのブラウザやプロキシサーバーに、このページを86,400秒(丸一日!)保持するように指示していたのだ! Ageヘッダーは、提供されているバージョンがすでに9時間以上古いことを示していた。彼らが使っているCDN(コンテンツデリバリーネットワーク)の設定ミスで、すべてのHTMLページに強力なキャッシュポリシーが適用されていたのだ。彼らがCDNのルールを修正すると、すぐに全員に新しいページが表示されるようになった。

教訓: レスポンスヘッダーは単なるメタデータではない。ブラウザ、プロキシ、CDNを制御する指示書だ。Cache-Control、Expires、ETagを理解することは、コンテンツがどのように配信されるかを管理する上で非常に重要だ。

沈黙のボディ・スナッチャー

あるジュニア開発者が、ユーザーからのフィードバックを受け付けるためのシンプルなAPIエンドポイントを構築した。彼のローカルマシンでは完璧に動作した。しかし、ステージング環境ではフォームの送信が失敗していた。サーバーログにはPOST /feedbackリクエストが到着していることは記録されていたが、リクエストボディは常に空だった。ユーザーデータが跡形もなく消えていたのだ。

途方に暮れた彼は、サーバーに到着した生のHTTPリクエスト全体をダンプしてみた。テスト送信で、彼はこれを見た:

POST /feedback HTTP/1.1
Host: staging.myapp.com
Content-Type: application/json
Content-Length: 0

{}

しかし、彼はクライアント側のコードが完全なJSONオブジェクトを送っていることを知っていた! Content-Lengthが0で、ボディは空だった。原因を遡っていくと、ステージング環境のWeb Application Firewall(WAF)に、未知のパスへのPOSTリクエストからボディを削除するという、誤って設定されたセキュリティルールがあることを発見した。/feedbackは新しいエンドポイントだったため、WAFがデータを静かに食べてしまうことでサーバーを「保護」していたのだ。

教訓: Content-LengthとContent-Typeヘッダーは、クライアントとサーバー間の契約書だ。これらがボディを正確に記述していないと、不可解な形でシステムが壊れる。データ転送の問題をデバッグするときは、常にこれらを確認すること。

よくある間違いと落とし穴

  • 空行を忘れる。 ヘッダーとボディの間にある空行(CRLFCRLF)は、ただの空白ではない。それは基本的な区切り文字だ。これがないと、メッセージ全体が不正な形式となり、サーバーはヘッダーがどこで終わり、ペイロードがどこから始まるのかを知ることができない。
  • Content-Lengthの不一致。 ヘッダーでContent-Length: 100と宣言しているのに、50バイトのボディしか送らなかった場合、サーバーはタイムアウトするまで残りの50バイトを待ち続ける。もし150バイト送った場合、余分な50バイトは新しい、文字化けしたリクエストの始まりとして誤解されるかもしれない。
  • CRLFとLFの改行コードの違い。 HTTPの仕様は厳格だ。行はキャリッジリターン(\r)に続いてラインフィード(\n)で終わらなければならない。多くの現代的なサーバーは寛容で、単純なラインフィード(\n)も受け入れるが、古いサーバーや厳格なサーバーはメッセージを拒否したり、正しく解析できなかったりすることがある。
  • Content-Typeを無視する。 POSTのボディに完璧に有効なJSONオブジェクトを送っていても、Content-Type: application/jsonヘッダーを含めないと、サーバーはそれがapplication/x-www-form-urlencoded(フォームのデフォルト)だと勘違いして、解析に失敗するかもしれない。
  • ヘッダーの大文字小文字の混同。 ヘッダーの名前は大文字小文字を区別しない(Content-Typeはcontent-typeと同じ)。しかし、ヘッダーの値は、しばしば大文字小文字を区別する。APIキーやBase64エンコードされた値などがその典型例だ。

なぜ知っておくべきか

ウェブ開発、API設計、あるいはネットワークセキュリティに関連する何かをするなら、生のHTTPメッセージを理解することは選択肢ではなく、基礎だ。君が使っている高レベルなフレームワークやライブラリは、こうした泥臭い部分をうまく隠してくれるが、それらが失敗したり予期せぬ動作をしたりしたときには、その層を剥がして生の通信を見ることができなければならない。

以下のような場面では、常に生のHTTPメッセージについて考えるべきだ:

  • ネットワーク関連のエラー(4xxや5xxコード)をデバッグするとき。
  • ウェブパフォーマンス(キャッシュ、圧縮)を最適化しようとするとき。
  • APIを構築したり利用したりするとき。
  • リダイレクト、プロキシ、ロードバランサーを設定するとき。
  • ウェブのセキュリティ脆弱性(例:ヘッダーインジェクション)を調査するとき。

これらのメッセージを読んで解釈する方法を知っていることは、整備士がエンジンの仕組みを知っているようなものだ。毎回車を運転するときに考える必要はないが、車が故障したときには、本当に何が起こっているのかを突き止める唯一の方法となる。

もっと深く知るために

  • MDN: HTTP の概要 - 技術的な正確さと分かりやすさを両立させた、最良の入門資料です。
  • RFC 9110: HTTP Semantics - メソッド、ステータスコード、ヘッダーといったHTTPのコアコンセプトに関する現代の仕様書です。
  • RFC 9112: HTTP/1.1 - ここで議論したテキストベースのメッセージ構文を定義する仕様書です。
  • Wikipedia: Hypertext Transfer Protocol - HTTPの歴史と構成要素についての優れた高レベルの要約です。
  • HTTP/2 Explained - Daniel Stenberg氏(cURLの作者)による無料のオンライン書籍で、HTTPメッセージのコアコンセプトが、現代のバイナリプロトコルであるHTTP/2でどのように適用されているかを解説しています。

理論はOK。さあ手を動かそう — 100%ブラウザ内で。

ツールを試す: HTTP メッセージビューア