サーバーに「何言ってるかわからない」と言われないために。
400 Bad Request は、サーバーがリクエストを理解できなかったときに返すステータスコードです。このサイトでは、HTTPを「正しいリクエストを書けるようになる」ところまで、順番に学びます。
各章の最後に確認問題があります。正解すると左の目次にチェックが付き、進み具合はこのブラウザに保存されます。最終章は、本物っぽいリクエストを組み立てて 200 OK をもらう実技です。
CHAPTER 01HTTPってなに
HTTP(HyperText Transfer Protocol)は、クライアントとサーバーがやり取りするための約束ごとです。
ブラウザでページを開くとき、裏ではブラウザ(クライアント)がサーバーに「このページをください」というリクエストを送り、サーバーがレスポンスを返しています。HTTPはこの1往復を基本単位にした、とてもシンプルなプロトコルです。
3つの大事な性質
- リクエストとレスポンス: 必ずクライアントから話しかけ、サーバーが答えます。サーバーから勝手に話しかけることは(基本的に)ありません。
- ステートレス: サーバーは前のリクエストを覚えていません。1回1回が独立しています。ログイン状態などは Cookie などで別に持ち運びます(第7章)。
- テキストで読める(HTTP/1.1): HTTP/1.1 のメッセージは人間が読める文字列です。HTTP/2 以降はバイナリになりますが、意味(セマンティクス)は同じです。
RFC 9110(HTTP Semantics)にまとまっています。キャッシュは RFC 9111、HTTP/1.1 の書式は RFC 9112、HTTP/2 は RFC 9113、HTTP/3 は RFC 9114 です。CHAPTER 02URLの読み方
リクエストは「どこの、何を」から始まります。それを表すのがURLです(仕様は RFC 3986)。
| 部品 | 役割 |
|---|---|
| スキーム | http か https。どの方式で話すか。 |
| ホスト | 相手のサーバー名。DNSでIPアドレスに変換されます。 |
| ポート | 省略すると http は 80、https は 443。 |
| パス | サーバーの中の「どれ」。 |
| クエリ | ? 以降の キー=値。& でつなぎます。 |
| フラグメント | # 以降。サーバーには送られません。ページ内の位置などにブラウザが使います。 |
パーセントエンコーディング
URLに使えない文字(日本語やスペースなど)は % と16進数で表します。たとえば「あ」は UTF-8 で3バイトなので %E3%81%82、スペースは %20 です。変な文字をそのまま入れると、サーバーによっては 400 を返します。
CHAPTER 03リクエストの中身
HTTP/1.1 のリクエストは、リクエスト行・ヘッダー・空行・ボディの4つでできています。
GET /learn/http?ch=3 HTTP/1.1 ← リクエスト行(メソッド・パス・バージョン) Host: 400.pages.dev ← ヘッダー(ここから) User-Agent: Mozilla/5.0 ... Accept: text/html ← 空行(ヘッダーの終わり) ← ボディ(GETでは普通なし)
改行は本来 CR LF(\r\n)です。HTTP/1.1 では Host ヘッダーが必須で、無いとサーバーは 400 を返すことになっています。1つのIPアドレスで複数のサイトを動かせるのは、このHostのおかげです。
メソッド
| メソッド | 意味 | 安全 | 冪等 |
|---|---|---|---|
GET | 取得する | ○ | ○ |
HEAD | GETのヘッダーだけ | ○ | ○ |
POST | データを送って処理してもらう | × | × |
PUT | 指定の場所を丸ごと置き換える | × | ○ |
PATCH | 一部だけ変更する(RFC 5789) | × | × |
DELETE | 削除する | × | ○ |
OPTIONS | 使える機能を聞く(CORSで使う) | ○ | ○ |
CHAPTER 04レスポンスとステータスコード
サーバーの返事は、ステータス行・ヘッダー・空行・ボディ。最初の3桁の数字が結果を表します。
HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 1234 <!doctype html>...
| 範囲 | 意味 | ひとこと |
|---|---|---|
1xx | 情報 | まだ途中です |
2xx | 成功 | できました |
3xx | リダイレクト | 別の場所へどうぞ |
4xx | クライアントエラー | あなたのリクエストが悪い |
5xx | サーバーエラー | こっちが悪い |
よく見るコード(タップで説明)
400。誰かわからない → 401。誰かはわかるけどダメ → 403。無い → 404。形式は正しいけど中身の意味が通らない → 422。送りすぎ → 429。CHAPTER 05ヘッダー
ヘッダーはメッセージに付ける付せんです。名前: 値 の形で、名前の大文字小文字は区別しません。
| ヘッダー | 向き | 役割 |
|---|---|---|
Host | リクエスト | 宛先のホスト名 |
User-Agent | リクエスト | クライアントのソフト名 |
Accept | リクエスト | 欲しい形式(text/html など) |
Accept-Language | リクエスト | 欲しい言語(ja,en;q=0.8) |
Authorization | リクエスト | 認証情報(Bearer ...) |
Content-Type | 両方 | ボディの形式 |
Content-Length | 両方 | ボディのバイト数 |
Location | レスポンス | リダイレクト先(3xx) |
Set-Cookie | レスポンス | Cookieを保存させる |
Cache-Control | 両方 | キャッシュの方針 |
コンテンツネゴシエーション
同じURLでも、Accept や Accept-Language によってサーバーが返す中身を変えられます。q=0.8 は「優先度0.8」という意味で、1が最優先です。
content-type と Content-Type の関係は?CHAPTER 06ボディとContent-Type
ボディは本体。何が入っているかを Content-Type(MIMEタイプ)で伝えます。
| Content-Type | 中身 |
|---|---|
text/html; charset=utf-8 | HTMLページ |
application/json | JSON(APIでよく使う) |
application/x-www-form-urlencoded | HTMLフォームの標準。a=1&b=2 |
multipart/form-data | ファイルアップロード付きフォーム |
image/png など | 画像 |
POST /api/notes HTTP/1.1 Host: 400.pages.dev Content-Type: application/json Content-Length: 18 {"text":"hello"}
Content-Type を付け忘れる。JSONの最後に余計なカンマがある。Content-Length が実際の長さと合わない。どれもサーバーから見ると「読めないリクエスト」です。CHAPTER 07Cookieと状態
ステートレスなHTTPで「ログインしたまま」を実現する仕組みが Cookie です(RFC 6265)。
Set-Cookie の属性
| 属性 | 意味 |
|---|---|
Expires / Max-Age | 有効期限。無ければブラウザを閉じると消える |
Domain / Path | どのURLに送るか |
Secure | HTTPSのときだけ送る |
HttpOnly | JavaScriptから読めなくする(盗まれにくくなる) |
SameSite | 他サイトからのリクエストに付けるか(Strict / Lax / None) |
pages.dev は Public Suffix List に載っているので、400.pages.dev が Domain=pages.dev のCookieを作ることはできません。他人の 705.pages.dev にCookieを押し付けられないようにするためです。CHAPTER 08キャッシュ
同じものを何度も取りに行かないための仕組みです(RFC 9111)。
新鮮さ:Cache-Control
| 指定 | 意味 |
|---|---|
max-age=3600 | 3600秒は新鮮。サーバーに聞かずに使ってよい |
no-cache | 保存はいいけど、使う前に毎回サーバーに確認して |
no-store | そもそも保存しないで |
private / public | ブラウザだけ / CDNなど共有キャッシュもOK |
immutable | 期限内は絶対に変わらない |
no-cache は「キャッシュしない」ではない、というのが一番の落とし穴です。
検証:ETag と 304
// 1回目 ← 200 OK ETag: "v42" // 2回目(期限切れ後) → GET /style.css If-None-Match: "v42" ← 304 Not Modified (ボディなし。手元のを使って)
CHAPTER 09HTTPSとTLS
HTTPS は、HTTP を TLS という暗号化の層の上で動かしたものです。HTTPそのものの書き方は変わりません。
- 盗み見防止: 通信内容が暗号化されます。パスやヘッダー、Cookieも見えません。
- 改ざん防止: 途中で書き換えられたら気付けます。
- なりすまし防止: サーバー証明書で「本当に 400.pages.dev か」を確認します。
SNI)が見えます。パス・クエリ・ヘッダー・ボディは見えません。SNIまで隠す仕組みとして ECH(Encrypted Client Hello)があります。HSTS
Strict-Transport-Security: max-age=31536000 を返すと、ブラウザは以後そのサイトに http で接続しなくなります。ちなみに .dev ドメインはまるごとHSTSプリロードされているので、400.pages.dev は最初から HTTPS 専用です。
CHAPTER 10HTTP/1.1・HTTP/2・HTTP/3
意味は同じまま、運び方が進化してきました。
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| 形式 | テキスト | バイナリ(フレーム) | バイナリ(フレーム) |
| 下の層 | TCP | TCP + TLS | QUIC(UDP、TLS 1.3内蔵) |
| 同時に複数 | 基本1本ずつ | 1接続で多重化 | 1接続で多重化 |
| ヘッダー圧縮 | なし | HPACK | QPACK |
| 仕様 | RFC 9112 | RFC 9113 | RFC 9114 |
ヘッドオブラインブロッキング
HTTP/1.1 では前のレスポンスが終わるまで次が詰まります。HTTP/2 は1本の接続に複数のストリームを混ぜて解決しましたが、下のTCPでパケットが1つ落ちると全ストリームが止まる問題が残りました。HTTP/3 はTCPをやめてQUICにすることで、落ちたストリームだけが待つようにしました。
HTTP/2 では Host の代わりに :authority、リクエスト行の代わりに :method :path :scheme という疑似ヘッダーを使います。
CHAPTER 11CORSとオリジン
ブラウザの中のJavaScriptが、別のサイトのAPIを勝手に読めないようにする仕組みです。
オリジン=スキーム+ホスト+ポートの組。https://400.pages.dev と https://705.pages.dev は別オリジンです。ブラウザは、別オリジンへの fetch の結果を、相手サーバーが許可したときだけJavaScriptに渡します(同一オリジンポリシー)。
→ GET /api/data Origin: https://400.pages.dev ← 200 OK Access-Control-Allow-Origin: https://400.pages.dev
プリフライト
PUT や Content-Type: application/json のような「単純でない」リクエストの前には、ブラウザが自動で OPTIONS を送り「このメソッドとヘッダーで送っていい?」と確認します。
CHAPTER 12実技:200 OKをもらえ
このサーバーはとても厳しい。ここまでの知識で、正しいリクエストを組み立ててください。
ここにあるのはトップページだけ。見るだけのつもりで、HTTP/1.1 の決まりは守って、宛先は正しく。それと、このサーバーは礼儀にうるさいです。