標準編 第12回 HTTP/1.1・HTTP/2 詳細 で学んだ HTTP/1.1 の仕様を 「実際に手を動かして動かす」 発展回です。telnet で生バイト列を流し、curl でメソッド・ヘッダ・Cookie を制御し、Python の http.client / requests / aiohttp、Node.js の http / fetch でクライアントとサーバを書きます。「ブラウザがやっている魔法」を分解して、HTTP/1.1 の挙動を実装視点で体得しましょう。
本講を終えると、以下を達成できるようになります。
図の見方:下に行くほど抽象度が高くなり、書きやすくなる代わりに「中で何が起きているか」が見えにくくなる。本講は ① ② ③ を中心に扱い、④ で実用感を、⑤ はさわりだけ紹介します。
つながる知識: 「自分で書く」は理解だけでなく 運用 の質を上げます。例えばブラウザだと再現できないバグも、curl なら同じヘッダを送り続けてサーバの挙動を切り分けられます。プロのサポートエンジニアは ブラウザより先に curl を開く と言われるほどです。
確認: ブラウザで再現できないが curl なら再現できるバグがあるとする。最もあり得るシナリオは?
正解:B。ブラウザは多数のヘッダを自動付与し、CORS や cache 制御も裏で実施する。curl は最小限の HTTP しか送らないので、サーバが「特定のヘッダがあること」「特定のオリジンから来ること」を前提に挙動を変えていると、curl の結果は別物になる。これを使えば「サーバはどのヘッダで挙動を変えているか」を切り分け解析できる ─ プロのデバッグツールとして curl が選ばれる本質的理由。
$ telnet example.com 80 Trying 23.215.0.136... Connected to example.com. Escape character is '^]'.
telnet は 引数で指定したホスト・ポート に TCP 接続するだけのツール。ここまでで TCP の 3-way ハンドシェイクが完了しています。下層は 標準編 TCP 参照。
GET / HTTP/1.1
「メソッド SP URI SP HTTP-バージョン」。Enter キーを押すと CRLF が送信されます(端末によっては LF だけの場合があるので、後述の nc -C のような工夫が必要)。
Host: example.com
HTTP/1.1 で必須の Host ヘッダ。同じ IP に複数のサイトが同居している前提なので、これを書かないと「どのサイトの / を要求しているのか」サーバに伝わりません。
(ここで Enter だけを押す = CRLF だけの行)
これが「ヘッダ終わり、本文に入ります」という合図です。GET には本文がないので、空行の直後にサーバが処理を開始します。
HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 1256 Date: Sun, 26 Apr 2026 03:21:08 GMT <!doctype html> <html> <head>…</head> …
サーバから ステータス行 → ヘッダ → 空行 → 本文 が流れてきます。Content-Length の数だけ本文を読めばよい、というのがクライアントの基本動作です。
Connection closed by foreign host.
HTTP/1.1 はパーシステント接続がデフォルトですが、Connection: close を付けるか、サーバのアイドルタイムアウトを過ぎるとサーバ側から切ってきます。telnet は ^](Ctrl+]) → quit でも切れます。
図の見方:HTTP メッセージは「行」がすべて CRLF で終わるテキストの並び。空行(\r\n だけの行) が「ここでヘッダが終わる」を示すデリミタになっている。空行を忘れるとサーバはずっと「もっとヘッダが来るかも」と待ち続け、応答が返ってこない。
Q. もし行末を LF(\n) だけ で送ったらどうなるでしょう? なぜ CRLF(\r\n) が必須なのか?
HTTP/1.1 の仕様(RFC 9112)では、行末は CRLF と明確に定められています。歴史的には ARPANET 時代から続くインターネットテキストプロトコルの慣習で、SMTP や FTP も同様です。
そのため、telnet で手打ちするときの 「Enter だけだと LF になる端末」 では応答が返ってこないことがあります。nc -C(BSD nc の CRLF オプション) や、Python から socket で b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" をそのまま送る方が確実です。改行コードのトラブルは HTTP プログラミングで最も頻出する地雷の1つです。
# 本文だけ標準出力 $ curl https://example.com/ # -v(verbose) で送信ヘッダ・受信ヘッダ・TLS の様子を全部見る $ curl -v https://example.com/
telnet で十数行手打ちしていた処理が、curl では 1行。-v を付ければ「自分が何を送ったか(>)」「サーバが何を返したか(<)」が両方見えるので、telnet 手打ちの代わりに HTTP の挙動を観察 する用途にも使えます。
初学者がよく混乱する 「同じことをやるのに書き方がツールごとに違う」 問題。example.com に GET を打つ という同じ動作を、4つのツールで並べてみます。
$ telnet example.com 80 GET / HTTP/1.1 Host: example.com (空行 ← ここで Enter)
最も低レベル。CRLF・空行・Host ヘッダを 全部自分で 用意する必要がある。
$ curl http://example.com/ # verbose 表示で何が起きているかを見る $ curl -v http://example.com/
1行で済む。Host ヘッダ・User-Agent・Accept は curl が自動で付ける。シェルスクリプトに組み込みやすい。
import requests
r = requests.get("http://example.com/")
print(r.status_code) # 200
print(r.headers["Content-Type"])
print(r.text[:200])
Python 側からアクセスして 結果をプログラムで処理 したいときの定番。レスポンスがオブジェクトとして扱える。
// Node.js 18+ の組み込み fetch
const res = await fetch("http://example.com/");
console.log(res.status); // 200
console.log(res.headers.get("content-type"));
console.log((await res.text()).slice(0, 200));
Node.js 18 以降は fetch がブラウザ互換 API として組み込まれた。サーバサイドでもブラウザと同じ書き方で使える。
図の見方:同じ HTTP リクエストでも、ツールが違えば書き方が大きく変わる。低レベル(telnet) ほど自由度が高く面倒、高レベル(fetch / requests) ほど書きやすいが内部が見えにくい。場面に応じて使い分ける のが上級者の道具立て。
つながる知識: 上の例の2つ目(curl)で出てくる -v 1個だけでも、HTTP の挙動観察にはかなり強力です。-H(ヘッダ追加)や -d(本文)を組み合わせて POST / Cookie / multipart まで扱えるようになると、ブラウザ操作の大半は再現できます。HTTPie のような「人にやさしい curl」も同じ思想の派生ツールとして使われます。
確認: curl でブラウザの動きを完全に再現したい。サーバ側のログを見ながら curl の挙動をブラウザに近づけるためのオプションとして、最も効果が大きいのはどれか?
正解:C。サーバの挙動はリクエストヘッダの組み合わせで決まることが多く、特に Cookie / User-Agent / Origin / Referer の影響が大きい。Chrome / Firefox の DevTools には 「Copy as cURL」 が標準搭載されており、ブラウザの実際のリクエストをすべて再現する curl コマンドが一発で取れる。本物のブラウザバグ調査でこれが最も使われる手段。-k はデバッグ目的の悪手(証明書問題を隠す)。
import http.client
# HTTPS なら HTTPSConnection、ポートも明示できる
conn = http.client.HTTPSConnection("example.com", 443, timeout=10)
conn.request(
method="GET",
url="/",
headers={"User-Agent": "myapp/1.0", "Accept": "text/html"},
)
res = conn.getresponse()
print(res.status, res.reason) # 200 OK
print(res.getheader("Content-Type")) # text/html; charset=UTF-8
body = res.read().decode("utf-8")
print(body[:200])
conn.close()
標準ライブラリだけで動くのが利点。ただし Cookie 管理・JSON 変換・接続再利用は 自分で書く必要 があります。
インストール: pip install requests。コードが直感的で、Cookie・JSON・リダイレクト・タイムアウトの扱いが洗練されています。
import requests
# GET + クエリパラメータ
r = requests.get(
"https://api.example.com/items",
params={"q": "router", "limit": 10},
headers={"Accept": "application/json"},
timeout=10,
)
r.raise_for_status() # 4xx/5xx なら例外
data = r.json() # 自動で JSON パース
print(len(data))
# POST(JSON 本体)
r = requests.post(
"https://api.example.com/users",
json={"name": "alice", "age": 20}, # json= は自動で Content-Type と直列化
headers={"Authorization": "Bearer xxx"},
timeout=10,
)
print(r.status_code, r.json())
同じサイトに複数回アクセスするなら requests.Session を使います。これは HTTP/1.1 のパーシステント接続(前回参照) を裏で活用し、TCP/TLS の確立コストを節約します。さらに Cookie もまたいで自動的に運んでくれます。
import requests
with requests.Session() as s:
# 共通ヘッダはここで一括設定
s.headers.update({"User-Agent": "myapp/1.0"})
# ① ログイン:Set-Cookie で session ID をもらう
s.post("https://example.com/login",
data={"user": "alice", "pw": "secret"})
# ② 以降のリクエストは Cookie が自動で付く
r = s.get("https://example.com/mypage")
print(r.status_code)
# ③ 同じセッション内では TCP 接続も再利用される(高速)
for i in range(100):
s.get(f"https://example.com/items/{i}")
裏側はけっこう複雑なことをやっています。だからこそ「Session を使わずにグローバル requests.get をループで回す」と、毎回 TCP/TLS 確立で大幅に遅くなるという初学者の罠が生まれます。
import requests
from requests.exceptions import (
Timeout, ConnectionError, HTTPError, RequestException,
)
try:
r = requests.get("https://api.example.com/items", timeout=(5, 30))
r.raise_for_status()
data = r.json()
except Timeout:
print("タイムアウト(接続 5s / 受信 30s)")
except ConnectionError as e:
print(f"接続失敗: {e}")
except HTTPError as e:
print(f"HTTP エラー: {e.response.status_code}")
except RequestException as e:
print(f"その他: {e}")
timeout=(connect, read) のようにタプルで分けて指定できます。タイムアウト未指定だと無限に待つ ので本番コードでは必ず付けましょう。
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as r:
return url, r.status, len(await r.read())
async def main(urls):
async with aiohttp.ClientSession() as session:
# 全リクエストを「同時に」開始してまとめて待つ
results = await asyncio.gather(
*(fetch(session, u) for u in urls),
return_exceptions=True,
)
for r in results:
print(r)
urls = [f"https://example.com/{i}" for i in range(100)]
asyncio.run(main(urls))
# カレントディレクトリを 8000 番ポートで配信 $ python -m http.server 8000 # 別端末から確認 $ curl http://localhost:8000/ $ curl http://localhost:8000/index.html
HTML / JS / 画像をローカルで動作確認したい時の定番。ディレクトリリスティング も自動で出してくれます(本番環境ではセキュリティ的に NG)。
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/hello":
body = json.dumps({"message": "Hello!"}).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers() # ← 空行を送信
self.wfile.write(body)
else:
self.send_error(404, "Not Found")
def do_POST(self):
n = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(n) # ← Content-Length の通り読む
# … body をパースして処理 …
self.send_response(201)
self.end_headers()
if __name__ == "__main__":
HTTPServer(("0.0.0.0", 8000), MyHandler).serve_forever()
do_GET・do_POST をメソッドで定義すれば、メソッドごとの処理がそのままマッピングされます。end_headers が 空行(CRLF) を送る点に注目 ── 前のセクションで強調した「ヘッダ終わりの合図」と一致しています。
業務での Web アプリ・API は フレームワーク を使います。標準ライブラリ版より圧倒的に書きやすく、ルーティング・バリデーション・テストも整っています。
# pip install flask
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.get("/hello")
def hello():
return jsonify(message="Hello!")
@app.post("/users")
def create_user():
body = request.get_json()
return jsonify(id=1, **body), 201
if __name__ == "__main__":
app.run(port=8000)
シンプル・歴史も長い。学習・小規模アプリの定番。
# pip install fastapi uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
name: str
age: int
@app.get("/hello")
async def hello():
return {"message": "Hello!"}
@app.post("/users", status_code=201)
async def create_user(user: User):
return {"id": 1, **user.model_dump()}
# 起動: uvicorn main:app --port 8000
型ヒントから自動でバリデーションと OpenAPI ドキュメント生成。非同期サポート。新規プロジェクトでの人気が高い。
// npm install express
const express = require("express");
const app = express();
app.use(express.json());
app.get("/hello", (req, res) => {
res.json({ message: "Hello!" });
});
app.post("/users", (req, res) => {
res.status(201).json({ id: 1, ...req.body });
});
app.listen(8000);
Node.js 圏のデファクト。ミドルウェア合成型の設計で、巨大なエコシステムを持つ。
3つとも「/hello に GET したら JSON を返す」だけのコードですが、設計思想に違いがあります。
選ぶ基準は 言語・性能要件・開発規模・チームの慣れ。プロトタイプは Flask、本番 API は FastAPI、フロントエンドと同居なら Express、というのが2026年時点の典型的な棲み分けです。[要確認] 最新版番号や微細な API 差は各公式ドキュメント参照。
// node server.js で起動。Node.js 18+ 推奨
const http = require("http");
const server = http.createServer((req, res) => {
console.log(`${req.method} ${req.url}`);
if (req.url === "/hello") {
const body = JSON.stringify({ message: "Hello!" });
res.writeHead(200, {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
});
res.end(body);
} else {
res.writeHead(404).end("Not Found");
}
});
server.listen(8000, () => console.log("listening on :8000"));
コールバックの (req, res) がリクエスト・レスポンスのオブジェクト。req.method / req.url / req.headers で受信内容を読み、res.writeHead + res.end で応答を送ります。
// GET
const r = await fetch("https://api.example.com/items?q=router");
console.log(r.status);
const data = await r.json();
// POST JSON
const r2 = await fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer xxx",
},
body: JSON.stringify({ name: "alice", age: 20 }),
// タイムアウトは AbortController で
signal: AbortSignal.timeout(10_000),
});
console.log(r2.status, await r2.json());
ブラウザの fetch と 同じ書き方 で動きます。Node.js 17 以前を扱うなら node-fetch パッケージを使う必要がありました。
const https = require("https");
const req = https.request({
hostname: "api.example.com",
port: 443,
path: "/items",
method: "GET",
headers: { "Accept": "application/json" },
}, (res) => {
let body = "";
res.on("data", (chunk) => body += chunk);
res.on("end", () => {
console.log(res.statusCode, JSON.parse(body));
});
});
req.on("error", (e) => console.error(e));
req.end();
ストリーム指向の API。大きなレスポンスを 逐次処理 したい時はこちらが向きます(全文をメモリに乗せない)。
つながる知識: Python の http.client と Node.js の http.request は、ともに 「TCP の上に直接 HTTP を喋る」 ための低レベル API です。書き方は違っても、やっていることは 「リクエスト行 + ヘッダ + 空行 + 本文を組み立てて socket に書き込む」 ── 第2節で telnet で手打ちしたのと同じ手順です。これが見えてくると、HTTP は怖くなくなります。
# Flask の例
from flask import Flask, make_response, request
import secrets
app = Flask(__name__)
SESSIONS = {} # 本番は Redis 等の外部ストア
@app.post("/login")
def login():
user = request.form["user"]
# … 認証処理 …
sid = secrets.token_urlsafe(32)
SESSIONS[sid] = {"user": user}
resp = make_response({"ok": True})
resp.set_cookie(
"session", sid,
httponly=True, # JS から触れない(XSS 対策)
secure=True, # HTTPS のみ送信
samesite="Lax", # CSRF 対策
max_age=3600,
)
return resp
@app.get("/mypage")
def mypage():
sid = request.cookies.get("session")
if sid not in SESSIONS:
return {"error": "unauthorized"}, 401
return {"user": SESSIONS[sid]["user"]}
requests / fetch とも、同じ Session / 同じドメインなら 自動で運んでくれる(サーバ実装と整合する)。手動で送りたいなら以下。
# Python 手動指定
requests.get("https://example.com/mypage",
cookies={"session": "abc123"})
# Node fetch 手動指定
await fetch("https://example.com/mypage", {
headers: { "Cookie": "session=abc123" },
});
SPA や REST API、特に複数オリジン跨ぎでは、Cookie より Authorization ヘッダで Bearer トークン を送る形式がよく使われます。
# クライアント側(requests)
TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6Ik..." # JWT 等
r = requests.get(
"https://api.example.com/me",
headers={"Authorization": f"Bearer {TOKEN}"},
)
# サーバ側(Flask)
from flask import request
@app.get("/me")
def me():
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return {"error": "missing token"}, 401
token = auth[7:]
user = verify_token(token) # JWT 検証など
if not user:
return {"error": "invalid token"}, 401
return {"user": user}
ブラウザがドメイン単位で自動管理。HttpOnly で XSS による盗難を防げる(JS から触れない)。サーバ側にセッションストアが必要(状態を持つ)。同一サイト内の Web アプリに向く。
JS から自由に扱えるため、SPA・モバイル・他ドメイン API 連携に向く。JWT ならサーバ側状態を持たずに署名検証だけで認証できる(スケールしやすい)。半面、ストレージに置けば XSS で盗まれやすく、長期トークンの取扱いに注意が必要。
つながる知識: Cookie か Bearer かは「設計判断」であって HTTP の仕様としてはどちらも単なるヘッダ です。Set-Cookie / Cookie ヘッダは古くからの慣習で特別扱いされているだけで、自前の認証ヘッダ(X-API-Key など) を使う API も多数あります。詳細は TLS/PKI 標準編 や OAuth 関連の発展編で扱います。
確認: 同じ Web アプリでも、ブラウザ向けの認証は Cookie、API 向けの認証は Bearer トークンが選ばれることが多い。なぜ?
正解:C。Cookie の自動送信(同一オリジンならどんなリクエストにも勝手に乗る)は Web の利便性を支えるが、攻撃者が用意した別ドメインから誘発される CSRF の温床にもなる。SameSite 属性で軽減可能だが完全ではない。Bearer はクライアントが 明示的にヘッダにセットする 必要があり、自動送信されないため CSRF 経路を断てる ─ ただし XSS で盗まれるリスクは Cookie より大きいので一長一短。設計判断はトレードオフの中で行う。
| 道具 | 見える層 | 得意なシーン |
|---|---|---|
| curl -v | HTTP/1.1 のリクエスト・レスポンス全体 | ヘッダの値・ステータスコードを正確に確認したい時。最初の道具 |
| Chrome DevTools の Network タブ | ブラウザが投げた HTTP すべて | ブラウザ操作で再現する不具合の調査。タイミング・キャッシュ・Cookie が見やすい |
| HTTPie / Postman | HTTP/1.1 のメッセージ | API の対話的探索、ドキュメント例の試行 |
| Wireshark | TCP/IP のパケット全部 | 「そもそも TCP がつながってない」「TLS でコケてる」を切り分け |
| tcpdump | 同上(CLI 版) | サーバ側でリモートにキャプチャ。後で Wireshark で開く |
| mitmproxy | HTTPS 含む HTTP すべて | クライアント↔サーバ間に挟まり、TLS を解いて生 HTTP を見る・改変する |
| サーバアクセスログ | サーバが受けたリクエスト | 「リクエストが届いていない」のか「届いているが応答がおかしい」かの一次切り分け |
図の見方:アプリ層(curl)で問題が見えるか確認 → 見えなければサーバログ → 見えなければ TCP/IP 層 → 見えなければ中継機器、と 層を下から確認していく のが原則。やみくもに上の層で再現を試すより、下に降りた方が問題に近づくことが多い。
mitmproxy は HTTPS を含む HTTP トラフィックを 中間プロキシ として捕まえ、TLS を一旦解いて生メッセージを表示・改変できるツールです。
# インストール: pip install mitmproxy $ mitmproxy # TUI モード $ mitmweb # ブラウザ UI モード $ mitmdump # 標準出力にダンプ # クライアント側で proxy を mitmproxy のアドレスに向ける $ curl -x http://127.0.0.1:8080 -k https://api.example.com/items
mitmproxy の証明書をクライアントに信頼させた上で HTTPS を通すと、TLS 越しの中身を解読 してリクエストとレスポンスを表示します。デバッグには強力ですが、他人の通信に対して使うのは違法・倫理違反 です。必ず自分の権限がある通信だけに使ってください。
| 用語 | 1行説明 |
|---|---|
| telnet / nc | TCP に直接接続して生テキストを送る最小ツール。HTTP の学習用に最適 |
| CRLF | 行末コード \r\n。HTTP/1.x の行区切りで必須 |
| curl | HTTP プログラミングの定番 CLI。Cookie / 認証ヘッダ / multipart まで対応 |
| http.client | Python 標準ライブラリの低レベル HTTP クライアント |
| requests | Python のデファクト HTTP クライアント。直感的 API |
| Session | requests のセッションオブジェクト。TCP 再利用 + Cookie 自動管理 |
| aiohttp / httpx | 非同期 HTTP クライアント。高並列に向く |
| python -m http.server | 1コマンドで静的ファイル配信サーバを起動 |
| BaseHTTPRequestHandler | Python 標準の動的応答サーバ基底クラス |
| Flask / FastAPI | Python の主要 Web フレームワーク(同期/非同期) |
| http.createServer | Node.js の最小 HTTP サーバ API |
| fetch | ブラウザ・Node.js 18+ の標準 HTTP クライアント API |
| Bearer トークン | Authorization ヘッダで運ぶ認証情報。SPA / API で頻出 |
| mitmproxy | HTTPS を解いて中身を見る・改変する中間プロキシ |