FlowingDev

HTTPメッセージを解体新書:Webの生パケットを組み立てる

生のHTTPメッセージの構造を学び、Webクライアントとサーバーがどのように通信しているかを理解しましょう。スタートラインやヘッダーから、複雑なマルチパートボディまでを解説します。

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

一言で言うと

HTTPメッセージとは、Webブラウザとサーバーが交換する、整形されたただのテキストブロックのこと。Web上のあらゆるやり取りにおいて、送り状、取扱説明書、そして荷物の中身を兼ね備えたものとして機能します。

これが解決する問題

1990年代初頭のWebという原始のスープの中では、物事はシンプルでした。ブラウザはサーバーに「ねぇ、そのscience.htmlっていうファイルくれる?」と尋ねる方法が必要で、サーバーは「いいよ、どうぞ」とか「ごめん、見つからなかった」と返事する方法が必要でした。この会話にはルール、つまりプロトコルが必要でした。そのプロトコルがHTTP、Hypertext Transfer Protocolになったのです。

それが解決した「問題」とは、Webのための普遍的で曖昧さのない言語を作ることでした。標準的なフォーマットがなければ、あるサーバーはリクエストを1行で期待するかもしれないし、別のサーバーは俳句を要求するかもしれない。まさにカオスだったでしょう。初期のHTTP/0.9はめちゃくちゃシンプルで、GET /the-page-i-want.htmlと送るだけ。サーバーはただHTMLを返すだけでした。

しかし、Webはシンプルなままではいられませんでした。フォームに記入するために、データをサーバーに送信する必要が出てきました。画像や、後にはJSONのような様々なコンテンツタイプを扱う必要も出てきました。セキュリティ、キャッシュ、そしてブラウザが自分自身を説明する方法も必要でした。このシンプルな1行リクエストは、スタートライン、メタデータのブロック(ヘッダー)、そして実際のペイロードのためのオプショナルなボディを持つ、構造化されたマルチパートな「メッセージ」へと進化したのです。これらのメッセージを手作業で作成することは、Webインフラ、API、またはセキュリティに直接関わる人々にとって基本的なスキルとなり、Webのシンプルなリクエスト/レスポンスの対話の上で、ますます複雑化するビジネスを行う方法という問題を解決しました。

舞台裏の仕組み

HTTPメッセージの核心は、ただのテキストです。その気になれば、ターミナルに手打ちしてサーバーにパイプで送り込むことだってできます。このテキストは3つの部分に分かれています。スタートライン、ヘッダーのブロック、そしてオプショナルなボディで、これらはすべて特定の改行コード(\r\n、Carriage Return, Line Feedの略でCRLF)で区切られています。

メッセージには2つのタイプがあります:リクエスト(クライアントからサーバーへ)とレスポンス(サーバーからクライアントへ)です。見た目はほとんど同じですが、最初の行が異なります。

リクエストメッセージの構造

これは、あなたのブラウザが何かを要求するときのメッセージです。

GET /documentation/guides/http-builder HTTP/1.1
Host: flowing.dev
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0
Accept: text/html,*/*
Accept-Language: en-US,en;q=0.5
Connection: keep-alive

<-- ボディはここに来るが、GETリクエストには通常ない -->
  1. スタートライン: GET /documentation/guides/http-builder HTTP/1.1

    • GET: HTTPメソッド(または動詞)。あなたがしたいことです。GETはデータを取得し、POSTは新しいデータを送信し、PUTは既存のデータを更新し、DELETEはデータを削除します。
    • /documentation/...: リソースパス。Hostヘッダーと組み合わせることで、完全なURLを形成します。
    • HTTP/1.1: プロトコルバージョン。
  2. ヘッダー: リクエストに関する重要なメタデータを提供するキーと値のペアのリストです。

    • Host: flowing.dev: このリクエストは誰宛て? このヘッダーはHTTP/1.1では必須です。
    • User-Agent: Mozilla/5.0...: このリクエストを送っているのは誰? ブラウザが自己紹介しています。
    • Accept: text/html,*/*: どんなレスポンス形式を理解できる? ここでは、ブラウザはHTMLを好みますが、何でも受け入れます。
  3. 空行: 最後のヘッダーの後、1つの空行(\r\n)が「ヘッダーはここまで、次はボディ」という合図になります。これは交渉の余地なし。省略すればすべてが壊れます。

  4. ボディ: 実際のデータペイロードです。GETリクエストの場合、通常は空です。POSTやPUTの場合、フォームデータやJSONペイロードがここに入ります。

レスポンスメッセージの構造

これは、サーバーがリクエストに返信するときのメッセージです。

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 15328
Server: Vercel
Date: Mon, 25 Sep 2023 10:30:00 GMT
Cache-Control: public, max-age=0, must-revalidate

<!DOCTYPE html>
<html>
  <head>...</head>
  <body>...</body>
</html>
  1. ステータスライン: HTTP/1.1 200 OK

    • HTTP/1.1: プロトコルバージョン。リクエストと同じです。
    • 200: ステータスコード。結果を要約する3桁の数字です。2xxは成功、3xxはリダイレクト、4xxはあなた(クライアント)のミス、5xxは私(サーバー)のミスを意味します。
    • OK: 理由フレーズ。ステータスコードの人間が読める要約です。
  2. ヘッダー: レスポンスに関するメタデータです。

    • Content-Type: text/html: 「これから送るボディはHTMLですよ」という意味。ブラウザがペイロードをどうレンダリングすればよいか知るために非常に重要です。
    • Content-Length: 15328: 「ボディの長さは正確に15,328バイトです」という意味。
    • Set-Cookie: ...: サーバーがブラウザにクッキーを保存するよう指示する方法です。
    • Cache-Control: ...: ブラウザや中間プロキシがこのレスポンスをどのようにキャッシュすべきかの指示です。
  3. ボディ: クライアントが要求したリソースです—HTML、CSS、JSONオブジェクト、画像データなど。

ボディのあれこれ:ペイロードのエンコーディング

リクエストにボディがある場合、そのフォーマットを説明するためにContent-Typeヘッダーが必要です。最も一般的な3つは次のとおりです。

  • application/x-www-form-urlencoded: 昔ながらのHTMLフォームのデフォルト。ボディにクエリストリングが入っているだけです。

    name=Grace+Hopper&title=Rear+Admiral
    
  • application/json: 現代のAPIの王様。ボディはJSON文字列です。

    {
      "name": "Grace Hopper",
      "title": "Rear Admiral"
    }
    
  • multipart/form-data: ファイルアップロードを含むフォームを送信するためのフォーマット。メッセージの中のメッセージのようなものです。ボディは複数のパートに分割され、それぞれが「バウンダリ」文字列で区切られます。各パートは、独自のミニヘッダー(Content-DispositionやContent-Typeなど)と独自の中身を持つことができます。

    POST /profiles/edit HTTP/1.1
    Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
    
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="username"
    
    ada_lovelace
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="avatar"; filename="portrait.jpg"
    Content-Type: image/jpeg
    
    <...ここに画像の生のバイナリデータが入る...>
    ----WebKitFormBoundary7MA4YWxkTrZu0gW--
    

現場での実話

消えたContent-Type事件

ある開発者が初めてのREST APIを構築していました。エンドポイントは、新しいユーザーを作成するためにJSONペイロードを受け取るはずでした。彼はサーバーコードを書き、コマンドラインツールでテストし、完璧に有効なJSONオブジェクトを送信しました。しかし、サーバーは400 Bad Requestを返し続けました。彼は2時間も自分のJSONを睨みつけ、コンマを付け忘れたに違いないと確信していました。途方に暮れて、彼はシニア開発者に助けを求めました。シニア開発者はリクエストを一目見て尋ねました。「Content-Typeヘッダーはどこ?」 その開発者はJSONデータを送ってはいましたが、それがJSONであるとサーバーに伝えていなかったのです。サーバーフレームワークは、デフォルトのx-www-form-urlencodedを期待してJSONをクエリストリングとして解析しようとし、見事に失敗してリクエストを拒否したのでした。

教訓: メッセージのボディは、それに文脈を与えるContent-Typeヘッダーがなければ意味がありません。荷物には正しくラベルを貼らなければならない、というわけです。

マルチパートのごたごた

あるチームが、ユーザーが名前を変更し、任意で新しいプロフィール写真をアップロードできる「設定」ページを作成していました。若手のフロントエンド開発者は、これを2つの別々のAPIコールで実装しました:ユーザー名をJSONボディに入れたPUTリクエストと、写真が選択された場合は画像データを含むPOSTリクエストです。これは機能しましたが、ぎこちなく、レースコンディションを引き起こしていました。名前の変更は成功したのに、画像のアップロードが失敗したらどうなるでしょう?ユーザーは一貫性のない状態に取り残されてしまいます。バックエンドエンジニアがネットワークトラフィックを見て、彼を脇に呼びました。「これこそmultipart/form-dataの完璧なユースケースだよ」と彼女は説明しました。彼らはコードをリファクタリングし、名前フィールド用と画像ファイル用の2つのパートを持つ単一のPOSTリクエストを構築するようにしました。これによりコードが簡素化され、更新全体をアトミックな操作にすることができました。

教訓: multipartはファイル専用ではありません。テキストフィールド、ファイル、異なるコンテンツタイプなど、さまざまな種類のデータを単一の信頼できるリクエストで送信するためのものです。

キャッシュに潜むゴースト

あるEコマースサイトがフラッシュセールを実施していましたが、ユーザーから古い価格が表示されているという苦情が寄せられました。運用チームは困惑しました。サーバーサイドのキャッシュは正しく設定されているはずでした。Webパフォーマンスの専門家が呼ばれました。彼女はブラウザの開発者ツールを使わずに、製品ページの生のHTTPレスポンスを検査するツールを使いました。彼女はすぐに犯人を見つけました。Webサーバーの前にある設定ミスをしたロードバランサーが、独自のCache-Control: public, max-age=3600ヘッダーを注入し、サーバーが意図したCache-Control: no-cacheヘッダーを上書きしていたのです。このならず者ヘッダーは、アプリケーションサーバーが何と言おうと、ブラウザやCDNに価格を1時間キャッシュするように指示していました。

教訓: 生のHTTPメッセージは、究極の真実の源です。高レベルのツールは、テキストそのものを見れば一目瞭然な詳細を隠したり、誤って解釈したりすることがあります。

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

  • 空行を忘れる。 HTTPメッセージには、ヘッダーとボディの間にCRLF(\r\n)が必ず必要です。これが欠けていると、パーサーはあなたのボディを単なる不正なヘッダーだとみなし、リクエストは失敗します。
  • Content-Lengthの不一致。 Content-Lengthヘッダーを宣言する場合、その値はボディの正確なバイトサイズでなければなりません。小さすぎるとデータが切り捨てられ、大きすぎるとサーバーは決して届かないバイトを永遠に待ち続けることになります。
  • 間違ったContent-Type。 JSONボディを送りながら、text/plainとラベル付けするのは、4xxエラーを招く典型的なパターンです。ヘッダーとボディは一致しなければなりません。
  • CRLF vs. LF。 公式仕様では改行に\r\nを要求しています。ほとんどの現代的なサーバーは寛容で、単純な\n(Line Feed)も受け入れます。しかし、これに頼ると、古い、より厳格なサーバー、プロキシ、またはファイアウォールでリクエストが失敗する可能性があります。
  • 特殊文字のエンコーディング。 クエリストリングやx-www-form-urlencodedボディ内のデータをURLエンコードし忘れるのは、古典的なバグです。スペースは%20に、&は%26にするなどしないと、データが破損するリスクがあります。

なぜ知っておくべきか

ほとんどの場合、ブラウザ、フレームワーク、またはライブラリ(axiosやrequestsなど)がHTTPメッセージの作成という面倒な詳細を処理してくれます。しかし、次のような場合には手動で作成する方法を知っておくべきです。

  • デバッグの深みにいるとき。 APIコールが機能せず、エラーメッセージが曖昧な場合、生のHTTPメッセージを検査または再作成することが最終的な審判となります。これにより、あらゆる抽象化から解放された、実際にネットワーク上を流れるものを正確に見ることができます。
  • APIを構築またはテストしているとき。 メッセージ構造を理解することは、優れたAPIエンドポイントを設計し、効果的な統合テストを書くための基本です。セキュリティテスターは、脆弱性を見つけるために、わざと不正な形式のメッセージを作成することに日々を費やしています。
  • ウェブサイトをスクレイピングしているとき。 実際のブラウザを模倣し、ボット対策を回避するためには、特定のヘッダー(User-Agent、Referer、Accept-*など)の組み合わせでリクエストを構築する必要がしばしばあります。
  • webhookを扱っているとき。 アプリケーションがStripeやGitHubのようなサービスからwebhookを受信するとき、あなたは生のHTTPリクエストの受信側になります。そのイベントに基づいて行動するためには、ヘッダー(例えば、セキュリティ署名のため)とボディを解析する必要があります。

HTTPメッセージをゼロから組み立てる方法を知っていることは、整備士が内燃機関の仕組みを知っているようなものです。毎日やることではありませんが、何か問題が起こったとき、その基礎知識はプライスレスです。

もっと詳しく

  • MDN: HTTP の概要 - 分かりやすく書かれたハイレベルなガイドで、ここから始めるのが最適です。
  • RFC 9112: HTTP/1.1 - HTTP/1.1メッセージ構文に関する主要な技術仕様書。内容は濃いですが、決定的な情報源です。
  • MDN: HTTP ヘッダー - すべての標準HTTPヘッダーについて、包括的で検索可能なリファレンスです。
  • MDN: POST - POSTリクエスト用のさまざまなContent-Typeボディの詳細を含む実践的なガイドです。
  • Wikipedia: Hypertext Transfer Protocol - HTTPの歴史と文脈に関するしっかりとした概要です。

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

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