第2章 HTTP は何をやりとりしているのか

第1章(本書の対象読者とゴール)では、ブラウザからレスポンスまでの全体像を俯瞰し、登場する役者たちを整理しました。 ここからはいよいよ、各層の中身に踏み込んでいきます。

第2章(HTTP は何をやりとりしているのか)の主役は HTTP です。 Web アプリケーションのあらゆる通信は、HTTP というプロトコルの上で行われています。 フレームワークが提供する request.GETresponse.status_code といった便利なオブジェクトも、突き詰めれば HTTP のテキストを Python オブジェクトに変換したものです。 まずは、その変換の元になっている「生のテキスト」の姿を見ていきましょう。

HTTP リクエストHTTP レスポンスリクエストラインGET /users/42/ HTTP/1.1ヘッダーHost: example.comUser-Agent: ...Connection: keep-alive空行 (CRLF)ボディ(POST/PUT のみ)ステータスラインHTTP/1.1 200 OKヘッダーContent-Type: application/jsonContent-Length: 62空行 (CRLF)ボディ{id: 42, ...} サーバが処理して返す

図2-1 HTTP リクエストとレスポンスの構造

2.1 リクエストライン

ブラウザがサーバに送る HTTP リクエストの最初の1行を、リクエストラインと呼びます。

GET /users/42/ HTTP/1.1

この1行には、3つの情報が空白で区切られて並んでいます。 メソッドGET)、リクエストターゲット/users/42/)、HTTP バージョンHTTP/1.1)です。

  • メソッド: 「何をしたいのか」を示します。GET は「リソースを取得したい」、POST は「データを送信したい」という意図を表します。メソッドについては本節の後半で改めて整理します。

  • リクエストターゲット: サーバ上のどのリソースに対するリクエストなのかを示すパスです。ブラウザのアドレスバーに入力した URL のうち、ドメイン名より後ろの部分がここに入ります。クエリ文字列がある場合は /search?q=python&page=2 のようにパスの後ろに付きます。

  • HTTP バージョン: このリクエストがどのバージョンの HTTP 仕様に従っているかを示します。

注釈

現在のウェブでは HTTP/2 が広く普及しており、さらに HTTP/3 も急速に広まっています。 HTTP/1.1 は一部のレガシー環境では今も使われていますが、シェアは大幅に低下しています。

HTTP/2 や HTTP/3 はメッセージ形式がバイナリベースであるため、HTTP/1.1 のように人間がそのままテキストとして読み解くことはできません。 本書では HTTP/1.1 を扱います。内部構造を理解するには、人間の目で読めるテキストプロトコルのほうがはるかに学びやすいからです。

リクエストラインの末尾には改行コード \r\n(キャリッジリターン+ラインフィード)が付きます。 HTTP の仕様では、行の区切りとして \r\n を使うことが定められています。

注意

Unix 系の OS で一般的な \n だけではなく、\r\n である点は、後ほどソケットで HTTP を自作するときに地味に重要になります。 うっかり \n だけで書いてしまうと、厳密な HTTP パーサーを持つクライアントやプロキシでは不正なメッセージとして扱われる可能性があります。

2.2 ヘッダー

リクエストラインの次に続くのが、ヘッダーです。ヘッダーはリクエストに関する付加情報を「名前: 値」のペアで伝えます。

GET /users/42/ HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

各行がひとつのヘッダーフィールドです。 コロンの左側がフィールド名、右側がフィールド値で、各行の末尾に \r\n が付きます。

代表的なヘッダーの役割をまとめると、次のとおりです。

表2-1 代表的なリクエストヘッダーの役割

ヘッダー名

役割

Host

このリクエストがどのドメインに向けたものかを示す。HTTP/1.1 では必須

User-Agent

リクエストを送っているクライアントの種類を伝える

Accept

クライアントが受け取れるコンテンツの形式(MIME タイプ)を伝える

Accept-Language

希望する言語を伝える

Accept-Encoding

対応している圧縮方式(gzip など)を伝える

Connection

TCP コネクションの維持と切断について指示する

Tip

Host ヘッダーは HTTP/1.1 で必須とされています。ひとつのサーバで複数のドメインをホストしている場合(バーチャルホスト)、サーバはこのヘッダーを見てどのサイトへのリクエストかを判断します。

Connection: keep-alive は、このリクエストの後も TCP コネクションを閉じずに維持してほしいという要求です。 HTTP/1.0 では1リクエストごとに TCP コネクションを張り直していましたが、HTTP/1.1 ではデフォルトで接続を維持する(持続的接続)ようになりました。

ヘッダーの一覧が終わると、空行\r\n だけの行)が入ります。 この空行が「ヘッダーの終わり」を示す合図です。 サーバはこの空行を見つけることで、ヘッダーの解析が完了したことを知ります。

2.3 ボディ

空行の後に続くのが、ボディ(メッセージ本文)です。 ただし、すべてのリクエストにボディがあるわけではありません。

  • GET リクエストは通常ボディを持ちません。

  • ボディが登場するのは主に POSTPUT など、クライアントからサーバにデータを送信するリクエストです。

たとえば、ユーザ登録フォームを送信する POST リクエストは次のような形になります。

POST /users/ HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 52

{"name": "Taro Yamada", "email": "[email protected]"}

ヘッダーの Content-Type はボディのデータ形式を示しています。

表2-2 Content-Type の値とボディの形式

Content-Type の値

ボディの形式

application/json

JSON データ

application/x-www-form-urlencoded

HTML フォームのデータ

multipart/form-data

ファイルアップロードを含むデータ

Content-Length はボディのバイト数を示します。 サーバはこの値を見て、「あと何バイト読めばボディが終わるか」を判断します。 この値がなければ、サーバはボディの終端を知る手段がありません(HTTP/1.1 には Transfer-Encoding: chunked という別の仕組みもありますが、基本は Content-Length です)。

重要

HTTP リクエスト全体の構造は、リクエストライン、ヘッダー、空行、ボディというシンプルな4要素で成り立っています。

リクエストライン\r\n ヘッダー1\r\n ヘッダー2\r\n …\r\n \r\n ← 空行(ヘッダーの終わり) ボディ(あれば)


この構造は HTTP レスポンスでもほぼ同じです(リクエストラインの代わりにステータスラインになるだけです)。

第3章(まずは 1 リクエストだけ処理するサーバを作る)で HTTP サーバを自作する際には、この構造をパースするコードを自分で書くことになります。

2.4 メソッド

リクエストラインに含まれるメソッドについて、もう少し整理しておきましょう。

HTTP の仕様では多くのメソッドが定義されていますが、Web アプリケーション開発で日常的に使うのは主に4つです。

表2-3 HTTP メソッドの安全性と冪等性

メソッド

用途

安全?

冪等?

GET

リソースの取得

POST

データの送信とリソースの作成

×

×

PUT

リソース全体の置き換え

×

DELETE

リソースの削除

×

用語解説: 安全と冪等

  • 安全(safe): サーバの状態を変更しないメソッドを「安全」と言います。GET は何度呼んでもデータが変わらないため安全です。

  • 冪等(べきとう): 同じリクエストを何度送っても結果が同じになるメソッドを「冪等」と言います。PUT は何度送っても同じデータで上書きされるだけなので冪等です。一方、POST は何度も送ると新しいリソースが複数作られる可能性があるため、冪等ではありません。

各メソッドをもう少し詳しく見てみましょう。

GET は、リソースの取得を要求するメソッドです。 ブラウザでページを開く、API からデータを取得する、といった操作はすべて GET です。 GET リクエストはボディを持たないのが原則で、サーバの状態を変更しない「安全な」メソッドとされています。

POST は、サーバにデータを送信して処理を要求するメソッドです。 フォームの送信、新しいリソースの作成などに使われます。 POST は安全でも冪等でもありません。 同じ POST リクエストを2回送ると、リソースが2つ作られる可能性があります。

PUT は、指定したリソースを送信したデータで置き換えることを要求するメソッドです。 リソース全体の更新に使われます。 PUT は冪等です。同じ PUT リクエストを何度送っても、結果は同じになります。

DELETE は、指定したリソースの削除を要求するメソッドです。 DELETE も冪等です。すでに削除されたリソースに対してもう一度 DELETE を送っても、結果は変わりません。

このほかに PATCH(リソースの部分的な更新)、HEAD(GET と同じだがボディを返さない)、OPTIONS(サーバが対応しているメソッドの問い合わせ)なども存在しますが、まずは GET と POST の2つをしっかり理解しておけば、本書を読み進めるうえで困ることはありません。

注釈

Django を使っている方は、ビュー関数の中で if request.method == 'GET': と分岐を書いたことがあるでしょう。 FastAPI であれば @app.get()@app.post() でデコレータを使い分けています。 これらは結局、HTTP リクエストラインの1つ目のフィールドを見て処理を振り分けているのです。

2.5 ステータスコード

ここまではリクエスト側の話でした。次は、サーバが返すレスポンスの中身を見てみましょう。

HTTP レスポンスの先頭行はステータスラインと呼ばれ、リクエストラインに対応する構造を持っています。

HTTP/1.1 200 OK

HTTP バージョン、ステータスコード200)、理由フレーズOK)の3要素です。 実質的に意味を持つのはステータスコードのほうで、理由フレーズは人間が読むための補助的な文字列です。

ステータスコードは3桁の数字で、先頭の1桁でカテゴリが決まります。

表2-4 HTTP ステータスコードのカテゴリ一覧

カテゴリ

意味

代表的なコード

1xx

情報(処理継続中)

101 Switching Protocols

2xx

成功

200 OK, 201 Created, 204 No Content

3xx

リダイレクト

301 Moved Permanently, 302 Found

4xx

クライアントエラー

400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found

5xx

サーバエラー

500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable

各カテゴリの意味を補足しておきます。

  • 1xx(情報): 処理が継続中であることを示します。日常の開発で目にする機会は少ないですが、WebSocket のアップグレードで使われる 101 Switching Protocols はこのカテゴリです。

  • 2xx(成功): リクエストが正常に処理されたことを示します。200 OK が最も基本的な成功レスポンスです。201 Created はリソースが新しく作成されたことを示し、204 No Content はボディなしの成功レスポンスで DELETE の応答などで使われます。

  • 3xx(リダイレクト): リクエストされたリソースが別の場所にあることを示します。ブラウザはこれらを受け取ると、Location ヘッダーに指定された URL に自動的に再リクエストを送ります。

  • 4xx(クライアントエラー): クライアント側に原因があるエラーです。1.6 節(なぜ「どの層の責務か」が重要なのか)で「404 はどの層で発生するか」を議論しましたが、ステータスコード自体はどの層が返しても同じ 404 です。だからこそ、層の切り分けが重要になります。

  • 5xx(サーバエラー): サーバ側に原因があるエラーです。502 Bad Gateway はリバースプロキシがバックエンドから不正なレスポンスを受け取った、504 Gateway Timeout はリバースプロキシがバックエンドからの応答を待ちきれなかった、という意味です。

レスポンス全体の構造は、リクエストとほぼ対称です。

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 62

{"id": 42, "name": "Taro Yamada", "email": "[email protected]"}

ステータスライン、ヘッダー、空行、ボディ。この4要素の組み合わせは、リクエストの構造と同じです。

重要

HTTP は、リクエストもレスポンスも同じ設計原則に基づいたテキストプロトコルです。

やりとりされるデータは、リクエストが「リクエストライン、ヘッダー、空行、ボディ」、レスポンスが「ステータスライン、ヘッダー、空行、ボディ」という、対称的でシンプルな構造をしています。この構造が、Web のあらゆる通信の基盤です。

次節では、この HTTP のテキストが実際にどうネットワーク上を流れるのか、その土台である TCP ソケットを見ていきます。

2.6 HTTP/1.1 の基本動作

前節では、HTTP リクエストとレスポンスの構造を確認しました。 リクエストライン、ヘッダー、空行、ボディ。この構造自体は HTTP/1.0 の時代から変わっていません。では、HTTP/1.1 は何が違うのでしょうか。

HTTP/1.1 は 1997 年に策定され、その後の改訂を経て、現在は 2022 年の RFC 9110〜9112 にまとめられています。今でも広く使われているバージョンです。 HTTP/1.0 からの最大の進化は、接続の効率化にあります。 HTTP/1.0 では、リクエストのたびに TCP コネクションを張って閉じるという無駄がありました。 HTTP/1.1 はこの問題を解決するために、いくつかの重要な仕組みを導入しました。

注釈

以下で取り上げる HTTP/1.1 の基本的な仕組みは、第3章(まずは 1 リクエストだけ処理するサーバを作る)で HTTP サーバを自作する際の実装上の判断に直結する知識です。

クライアント (ブラウザ)サーバ TCP 接続確立 (3ウェイハンドシェイク)GET /index.html HTTP/1.0200 OK (HTML)TCP 接続切断TCP 接続確立GET /style.css HTTP/1.0200 OK (CSS)TCP 接続切断TCP 接続確立GET /index.html HTTP/1.1200 OK (HTML, Content-Length あり)GET /style.css HTTP/1.1200 OK (CSS, Content-Length あり)GET /app.js HTTP/1.1200 OK (JS, Connection: close)TCP 接続切断

図2-2 HTTP/1.0 と HTTP/1.1 の接続管理の比較

HTTP/1.0 が接続を張り直すのに対し、HTTP/1.1 は同じ接続を再利用する持続的接続を行います。

2.6.1 keep-alive

HTTP/1.0 では、リクエストのたびに TCP 接続を張り直す必要がありました。 ブラウザが HTML ページを取得するために GET リクエストを送ります。 TCP の3ウェイハンドシェイクで接続を確立し、リクエストを送り、レスポンスを受け取り、接続を閉じます。 ところが HTML の中に CSS ファイル、JavaScript ファイル、画像ファイルへの参照が10個あったとします。 ブラウザはそれぞれのファイルを取得するために、同じサーバに対して10回、TCP コネクションの確立と切断を繰り返さなければなりません。

TCP の3ウェイハンドシェイクは、クライアントとサーバの間で3回のパケットのやりとりが必要です。 HTTPS であれば、さらに TLS ハンドシェイクが加わります。 たった1つのファイルを取得するために、この往復を毎回行うのは明らかに非効率です。

HTTP/1.1 では、持続的接続(persistent connection) がデフォルトの動作になりました。 一度確立した TCP コネクションを閉じずに維持し、同じコネクション上で複数のリクエストとレスポンスをやりとりできるようにしたのです。 これが俗に keep-alive と呼ばれる仕組みです。

表2-5 HTTP バージョンと持続的接続のデフォルト動作

バージョン

持続的接続のデフォルト

HTTP/1.0

オフ(使いたい場合は Connection: keep-alive を明示的に送る)

HTTP/1.1

オン(接続を閉じたい場合に Connection: close を送る)

GET /style.css HTTP/1.1
Host: example.com
Connection: close

このリクエストを受け取ったサーバは、レスポンスを返した後に TCP コネクションを閉じます。 Connection: close がなければ、サーバはコネクションを開いたまま次のリクエストを待ちます。

Tip

第3章(まずは 1 リクエストだけ処理するサーバを作る)で HTTP サーバを自作するとき、コネクションを閉じるタイミングを自分で判断しなければなりません。これは設計上の重要な分岐点になります。

2.6.2 Host ヘッダー

HTTP/1.1 で必須となったヘッダーが Host です。

GET /users/42/ HTTP/1.1
Host: example.com

なぜ Host が必須なのでしょうか。 リクエストターゲット(/users/42/)にはパスしか含まれておらず、どのドメインに対するリクエストなのかがわかりません。 TCP コネクションの接続先 IP アドレスはわかりますが、ひとつの IP アドレスで複数のドメインをホストしている場合(バーチャルホスト)、IP アドレスだけではどのサイトへのリクエストかを区別できません。

たとえば、ひとつのサーバで shop.example.comblog.example.com の2つのサイトを運用しているとします。 どちらも同じ IP アドレスに解決されます。 サーバは TCP コネクションを受け入れた時点では、どちらのサイトへのリクエストかわかりません。 Host ヘッダーを見て初めて、振り分けが可能になります。

実際のフレームワークとサーバでの使われ方

  • Nginx のバーチャルホスト設定(server_name ディレクティブ)は、まさにこの Host ヘッダーの値を見てリクエストの振り分け先を決定しています。

  • DjangoALLOWED_HOSTS 設定も、Host ヘッダーの値を検証するセキュリティ機構です。不正な Host ヘッダーを受け入れてしまうと、キャッシュポイズニングなどの攻撃に悪用される可能性があるため、Django はこの検証を厳格に行います。

HTTP/1.0 では Host ヘッダーはオプションでした。 HTTP/1.1 でこれが必須になったのは、インターネットの成長に伴いバーチャルホストが一般的になったためです。

2.6.3 Content-Length

前節でも触れましたが、Content-Length ヘッダーは HTTP/1.1 の動作を理解するうえで重要です。

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 62

{"id": 42, "name": "Taro Yamada", "email": "[email protected]"}

Content-Length は、ボディのバイト数を10進数で示します。 上の例では、JSON 文字列が正確に 62 バイトであることを伝えています。

なぜこの情報が必要なのでしょうか。 HTTP/1.0 で持続的接続を使わない場合、サーバがレスポンスを送り終えたら TCP コネクションを閉じます。 クライアントは「コネクションが閉じた=ボディが終わった」と判断できます。 しかし、HTTP/1.1 の持続的接続では、レスポンスを返してもコネクションは開いたままです。 クライアントは、ボディがどこで終わるのかを別の手段で知る必要があります。

Content-Length はその最も直接的な手段です。 クライアントは指定されたバイト数だけ読み取れば、「ここまでがこのレスポンスのボディで、この後に来るデータは次のレスポンスだ」と判断できます。

警告

Content-Length が示すのは文字数ではなくバイト数です。 ASCII 文字だけのボディであれば文字数とバイト数は一致しますが、日本語などのマルチバイト文字を含む場合は一致しません。

たとえば、UTF-8 でエンコードされた「山田太郎」は、4文字ですが 12 バイトです。 Content-Length を文字数で計算してしまうと、クライアントはボディの途中で読み取りを終えてしまいます。すると、残りのバイトが次のレスポンスの先頭として誤って解釈される、という深刻な事態を招きます。

Python でサーバを実装する際は、len(body) ではなく len(body.encode('utf-8')) を使うことを意識してください。

第3章(まずは 1 リクエストだけ処理するサーバを作る)で HTTP サーバを自作する際、この Content-Length の計算は Python のコードとして自分で書くことになります。 len("Hello")len("Hello".encode("utf-8")) の違いを意識することが、正しく動くサーバを作る鍵です。