Web と HTTP
読了目安 45 分
- 生の HTTP リクエストを読み書きできる
- 状態を持たない HTTP でログインが維持される仕組みを説明できる
- CORS エラーの原因を特定して直せる
Web アプリを作るということは、HTTP でやり取りするものを作るということです。
フロントエンドもバックエンドも、間にあるのは HTTP です。 「API が動かない」「なぜか 401 が返る」「CORS エラーが出た」—— 新人が最初にぶつかる問題のほとんどは、HTTP を知らないと解けません。
この章では、ブラウザの開発者ツールを開いて中身を読めるところまでを目指します。
URL は住所ではなく命令書
まず URL を分解します。何気なく見ているこの文字列には、意味のある部品が詰まっています。
https://api.example.com:443/items/42?sort=price&limit=20#reviews
└─┬─┘ └──────┬───────┘└┬┘└──┬───┘└────────┬─────────┘└──┬──┘
scheme host port path query fragment
| 部品 | 意味 |
|---|---|
| scheme | プロトコル。https は暗号化あり、http はなし |
| host | どのサーバーか。DNS で IP アドレスに変換される |
| port | サーバー上のどのプロセスか。省略時は http:80 / https:443 |
| path | サーバー内のどのリソースか |
| query | 絞り込みや並び順などの指定。? から始まり & で繋ぐ |
| fragment | サーバーには送られない。ブラウザ内でページ内位置を示すだけ |
# 以降はブラウザだけが解釈します。サーバーのログには残りません。
だから「URL にトークンを入れて共有する」設計を fragment でやると、 サーバー側では検証できません。逆に「サーバーに知られたくない情報」を置く用途には使えます。
クエリ文字列はアクセスログに残り、ブラウザの履歴にも残り、Referer で他サイトにも漏れます。
パスワードや API キーを ?token=xxx のように渡す設計は避けてください。
そういうものはヘッダかリクエストボディで送ります。
リクエストとレスポンス
HTTP は、テキストのやり取りです。ブラウザが送っているのは、実際にはこういう文字列です。
GET /items/42?sort=price HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGciOi...
User-Agent: Mozilla/5.0 ...
サーバーが返すのはこれです。
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store
Content-Length: 87
{"id":42,"name":"ワイヤレスイヤホン","price":8900}構造は3つです。
- 1行目 — リクエストなら「メソッド + パス」、レスポンスなら「ステータスコード」
- ヘッダ —
名前: 値の並び。付帯情報 - 空行のあとにボディ — 実際のデータ
これがすべてです。 GraphQL も gRPC も REST も、この上に載っているだけです。
メソッド
| メソッド | 用途 | 冪等か |
|---|---|---|
GET | 取得する。副作用があってはいけない | はい |
POST | 作成する。何度も送ると増える | いいえ |
PUT | 置き換える。同じ内容を何度送っても結果は同じ | はい |
PATCH | 一部だけ更新する | 実装次第 |
DELETE | 削除する。2回消しても「無い」状態は同じ | はい |
冪等(べきとう) とは「何度実行しても結果が同じ」という性質です。
なぜこれが重要かというと、通信は失敗するからです。 レスポンスが返ってこなかった時、リトライしていいかどうかは冪等かどうかで決まります。
「削除は簡単だから GET でいいや」と GET /items/42/delete のような API を作ると、
ブラウザやクローラが勝手にアクセスして削除します。
実際、これで全データが消えた事故は何度も起きています。 状態を変える操作は必ず POST / PUT / DELETE を使ってください。
ステータスコード
全部覚える必要はありません。先頭の数字が意味を持ちます。
| 範囲 | 意味 | 誰の問題か |
|---|---|---|
| 2xx | 成功 | — |
| 3xx | リダイレクト(別の場所を見て) | — |
| 4xx | クライアントの問題 | 送った側 |
| 5xx | サーバーの問題 | 受けた側 |
4xx と 5xx の境界が最も重要です。障害対応で最初に見るのがここで、 「4xx が増えた」ならクライアントの使い方かバリデーション、 「5xx が増えた」ならサーバーが壊れている、と切り分けられます。
覚えておくべきものだけ挙げます。
| コード | 意味 | よくある原因 |
|---|---|---|
| 200 | OK | |
| 201 | Created | POST でリソースを作成した |
| 204 | No Content | 成功したが返す中身がない(DELETE など) |
| 301 / 302 | 移動した | 301 は恒久、302 は一時 |
| 400 | Bad Request | リクエストの形式がおかしい |
| 401 | Unauthorized | 認証されていない(ログインしていない) |
| 403 | Forbidden | 認証はされたが権限がない |
| 404 | Not Found | リソースが無い |
| 409 | Conflict | 競合(重複登録など) |
| 429 | Too Many Requests | レート制限に引っかかった |
| 500 | Internal Server Error | サーバー側の例外。ログを見る |
| 502 / 503 | Bad Gateway / Unavailable | 後ろのサーバーが落ちている・過負荷 |
| 504 | Gateway Timeout | 後ろのサーバーが時間内に応答しなかった |
- 401 = あなたが誰か分からない → ログインしてください
- 403 = あなたが誰かは分かった。でも権限がない → 管理者に頼んでください
「ログインしているのに 401 が返る」なら、トークンが期限切れか、
そもそも送られていない可能性が高いです。開発者ツールで
Authorization ヘッダが付いているか確認してください。
API を呼んだら 500 が返りました。最初に見るべきものは?
ヘッダ
よく見るものだけ挙げます。
| ヘッダ | 意味 |
|---|---|
Content-Type | ボディの形式。application/json など |
Authorization | 認証情報。Bearer トークン の形が多い |
Accept | クライアントが受け取れる形式 |
User-Agent | クライアントの種類 |
Set-Cookie | サーバーがブラウザに Cookie を保存させる |
Cookie | ブラウザがサーバーに Cookie を送り返す |
Cache-Control | キャッシュの方針 |
JSON を送っているのに Content-Type: application/json を付けないと、
サーバーはボディをパースできず 400 を返します。
「curl では動くのにアプリからだと動かない」という時、 まずこのヘッダの有無を疑ってください。
状態を持たない HTTP で、なぜログインが維持されるのか
HTTP はステートレスです。1回のリクエストが終わったら、サーバーはあなたを忘れます。
ではなぜ、ページを移動してもログインしたままなのでしょうか。 毎回「自分が誰か」を証明する情報を送っているからです。
Cookie とセッション
1. ログイン POST /login { email, password }
2. サーバーが発行 Set-Cookie: session_id=abc123; HttpOnly; Secure
3. ブラウザが保存
4. 以降のリクエスト Cookie: session_id=abc123 ← 自動で付く
5. サーバーが照合 abc123 → ユーザーID 42 だな
サーバー側は「セッション ID → ユーザー」の対応表を持っています。 ログアウトはこの対応表から消すだけで、確実に無効化できます。
| Cookie の属性 | 意味 |
|---|---|
HttpOnly | JavaScript から読めない。XSS で盗まれるのを防ぐ |
Secure | HTTPS の時だけ送る |
SameSite | 別サイトからのリクエストで送るかどうか。CSRF 対策 |
トークン(JWT)
セッションの代わりに、署名付きのトークンを渡す方式もあります。
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0Mn0.xxxxx
サーバーは対応表を持たず、署名を検証するだけでユーザーを特定できます。 サーバーを増やしやすいので、マイクロサービスでよく使われます。
JWT は暗号化ではなく署名です。Base64 をデコードすれば中身は誰でも読めます。 秘密情報を入れてはいけません。
もう1つの弱点は、発行後に無効化しにくいことです。 サーバー側に対応表がないので「このトークンを失効させる」が難しく、 有効期限が切れるまで使えてしまいます。短い期限にするのが基本です。
キャッシュ
同じものを何度も取りに行かないための仕組みです。
| ヘッダ | 意味 |
|---|---|
Cache-Control: no-store | 保存しない(個人情報など) |
Cache-Control: no-cache | 保存はするが、毎回サーバーに聞く |
Cache-Control: max-age=3600 | 1時間はそのまま使ってよい |
ETag | 内容のバージョン。変わっていなければ 304 を返せる |
新人が高確率で踏みます。原因はキャッシュです。
- ブラウザのキャッシュ → スーパーリロード(
Cmd+Shift+R)で確認 - CDN のキャッシュ → パージが必要
- アプリ側のキャッシュ → TTL が切れるまで残る
まず「本当に新しいコードが動いているか」を確認してから原因を探してください。 デプロイしたのに古い JS が読まれている、というのは本当によくあります。
CORS — 実務で最も時間が溶けるエラー
独立した節にする価値があります。それくらい遭遇します。
何が起きているか
ブラウザには同一オリジンポリシーというルールがあります。
https://app.example.com で開いているページから
https://api.example.com へリクエストすると、ブラウザが結果を渡してくれません。
オリジンとは スキーム + ホスト + ポート の3点セットです。1つでも違えば別オリジンです。
https://app.example.com (基準)
https://api.example.com → ホストが違う 別オリジン
http://app.example.com → スキームが違う 別オリジン
https://app.example.com:8080 → ポートが違う 別オリジン
なぜそんな制限があるのか
もし制限がなければ、悪意あるサイトを開いただけで、 そのサイトの JavaScript があなたのログイン中の銀行サイトに勝手にリクエストを送り、 結果を読み取れてしまいます。それを防ぐための仕組みです。
どう解決するか
サーバー側が「このオリジンからは許可する」と応答ヘッダで宣言します。
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true新人が誤解しやすい点です。フロントエンドのコードをいくら直しても CORS は解決しません。 ブラウザがサーバーの応答ヘッダを見て判断しているからです。
「CORS エラーが出ました」と報告する時は、バックエンド側の設定の話だと理解しておくと、 話が早く進みます。
プリフライト
単純ではないリクエストの場合、ブラウザは本番のリクエストの前に OPTIONS を送って
許可を確認します。これがプリフライトです。
プリフライトが飛ぶ条件(代表的なもの):
GETHEADPOST以外のメソッド(PUTDELETEPATCH)Content-Type: application/jsonを使うAuthorizationなどのカスタムヘッダを付ける
つまり普通の JSON API はほぼ全部プリフライトが飛びます。
開発者ツールの Network タブで、同じ URL に対して OPTIONS と POST の
2行が出ていれば、それがプリフライトです。
よくある罠です。プリフライトの OPTIONS リクエストには認証情報が付きません。
サーバー側で「全リクエストに認証が必要」という設定をしていると、
OPTIONS が 401 を返し、本番のリクエストが送られる前に失敗します。
OPTIONS は認証の対象外にする必要があります。
フロントエンドから API を呼ぶと CORS エラーが出ました。どう対処しますか。
開発者ツールの Network タブ
実務で毎日使います。読み方を覚えてください。
Cmd+Option+I(Mac)で開き、Network タブを選んで、ページを再読み込みします。
| 見るところ | 分かること |
|---|---|
| Status | 200 か、4xx か、5xx か |
| Headers → Request Headers | 自分が何を送ったか(Authorization は付いているか) |
| Headers → Response Headers | サーバーが何を返したか(CORS ヘッダ、Set-Cookie) |
| Payload / Request | 送ったボディの中身 |
| Response | 返ってきた中身 |
| Timing | どこで時間がかかっているか |
先輩に聞く前に、この3つを見てください。答えが書いてあることが多いです。
- リクエストは飛んでいるか(Network に行が出ているか)
- ステータスは何か(4xx なら自分側、5xx ならサーバー側)
- 何を送っているか(Request Headers と Payload)
そして聞く時は、この3つを添えてください。第6章の質問テンプレートそのものです。
REST の考え方
「リソース」を URL で表し、「操作」をメソッドで表す設計スタイルです。
悪い例(操作を URL に書いている)
POST /getItem?id=42
POST /updateItemName
POST /deleteItem
良い例(リソース + メソッド)
GET /items/42
PATCH /items/42
DELETE /items/42
- URL は名詞(リソース)、メソッドが動詞(操作)
- 複数形を使う(
/items) - 階層で関係を表す(
/items/42/reviews)
REST の原理主義的な議論は現場ではあまり意味がありません。 大事なのはチーム内で一貫していることです。
既存の API がどういう形をしているかを先に見て、それに合わせてください。 新しい流儀を1人だけ持ち込むと、使う側が混乱します。
実務の落とし穴まとめ
- CORS はサーバー側で直す — フロントをいくら直しても解決しない
- OPTIONS を認証対象にしない — プリフライトが 401 で落ちる
- Content-Type の付け忘れ — JSON なのに 400 が返る
- GET で状態を変える — クローラに勝手に実行される
- クエリに秘密情報 — ログ・履歴・Referer に残る
- キャッシュを疑わない — 「直したのに反映されない」の主犯
- 401 と 403 を混同 — 認証と認可は別のもの
まとめ
- HTTP は「1行目 + ヘッダ + 空行 + ボディ」のテキスト。これがすべて
- 4xx はクライアント、5xx はサーバー。障害対応の切り分けはここから
- 401 = 誰か分からない、403 = 権限がない
- ステートレスな HTTP でログインが続くのは、毎回証明を送っているから
- Cookie は
HttpOnlySecureSameSiteを付ける。JWT の中身は誰でも読める - CORS はサーバー側の設定。プリフライト(OPTIONS)に認証を要求しない
- 困ったら開発者ツールの Network タブ。ステータス・リクエストヘッダ・ボディを見る
章末問題
ログイン済みなのに API が 401 を返します。まず確認すべきことは?
決済APIを呼んだが、レスポンスが返る前にタイムアウトしました。そのままリトライしてよいですか。
https://app.example.com のページから https://app.example.com:8443 の API を呼びました。CORS は発生しますか。
次の章では、このリクエストを実際に投げているブラウザの中で 何が起きているのかを見ていきます。