NET // communication networks
LESSON 29 / 発展編

HTTP プログラミング ─ 生 HTTP・curl・Python / Node で書く

標準編 第12回 HTTP/1.1・HTTP/2 詳細 で学んだ HTTP/1.1 の仕様を 「実際に手を動かして動かす」 発展回です。telnet で生バイト列を流し、curl でメソッド・ヘッダ・Cookie を制御し、Python の http.client / requests / aiohttp、Node.js の http / fetch でクライアントとサーバを書きます。「ブラウザがやっている魔法」を分解して、HTTP/1.1 の挙動を実装視点で体得しましょう。

学習目標

本講を終えると、以下を達成できるようになります。

本講は 標準編 HTTP/1.1・HTTP/2 詳細 の上に乗るハンズオン発展回です。HTTP のメッセージ構造・ヘッダ・ステータスコード・Cookie の仕組みは標準編ですでに学んだ前提で進めます。telnet による生 HTTP の打ち方は 標準編 telnet で覗くアプリ層プロトコル でも触れていますので、未習であればそちらを先に読むとスムーズです。HTTP/2 / HTTP/3 / TLS の実装 はそれぞれ 発展編 QUIC/HTTP3標準編 TLS/PKI に譲り、ここでは HTTP/1.1 を中心 に扱います。

このレッスンの目次

01 なぜ自分で書くか 普段 Web を使うとき、URL バーに https://example.com と… 02 生 HTTP を打つ HTTP/1.x はテキストベースのプロトコルなので、 TCP の上に正しい順序で文… 03 curl(紹介) curl は HTTP プログラミングの定番 CLI ツールです(macOS / L… 04 Python クライアント Python で HTTP を喋る方法は大きく2つ。標準ライブラリの http.cl… 05 aiohttp 非同期 数百〜数千の URL に同時にアクセスしたい場合(クローラ・ヘルスチェッカ・スクレイ… 06 最小サーバ クライアントだけでなくサーバも書けると、HTTP の理解が一段階深まります。Pyth… 07 Node.js Node.js は JavaScript ランタイムで、 サーバサイド JavaSc… 08 Cookie / 認証 前回(第12回)で Cookie とステートレス補完の 原理 を学びました。本節では… 09 デバッグ手段 HTTP のトラブルは多層に渡るため、 道具を使い分け て切り分けます。「どの層で問… 10 まとめ 本講の重要語句を整理 11 確認問題 理解度を問題でチェック

なぜ「自分で書く」のか ─ ブラウザの魔法を分解する

普段 Web を使うとき、URL バーに https://example.com と打てばページが表示されます。これは裏で 名前解決 → TCP 接続 → TLS ハンドシェイク → HTTP リクエスト → レスポンス受信 → HTML パース → 追加リソース取得 → レンダリング という長い処理が走っているからです。ブラウザはこの一連の作業を自動でやってくれる便利な道具ですが、「中で何が起きているか」を知らないと、トラブル時に手も足も出ません。
POINT HTTP を 自分で書く 経験は、以下の場面で必ず効きます。
トラブルシュート:「ブラウザでは見えるのに API が 401 を返す」を切り分ける
API クライアント実装:外部サービスや社内サービスに対して機械的に要求を出す
ボット・スクレイパ・自動テスト:ヘッダや Cookie を制御してアクセスする
観測と監視:エンドポイントのヘルスチェック、レイテンシ測定
セキュリティ調査:ヘッダ改竄、リプレイ、不正な値での挙動確認

道具立ての全体像

HTTP を扱う5つのレイヤと代表ツール ① 生バイト列 telnet, nc(netcat) ── 自分で1文字ずつ HTTP メッセージを打つ。学習用の最強の解像度 ② CLI ツール curl, wget, HTTPie ── ヘッダや Cookie を引数で指定して打てる。スクリプトに組み込みやすい ③ 標準ライブラリ Python http.client / urllib、Node.js http / https ── 言語に最初から入っている低レベル API ④ 高レベルライブラリ requests, httpx, aiohttp, axios, fetch ── Cookie・JSON・接続再利用を自動でやってくれる ⑤ サーバフレームワーク Flask, FastAPI, Express, Django ── ルーティング・ミドルウェア・ORM をまとめた応用層

図の見方:下に行くほど抽象度が高くなり、書きやすくなる代わりに「中で何が起きているか」が見えにくくなる。本講は ① ② ③ を中心に扱い、④ で実用感を、⑤ はさわりだけ紹介します。

つながる知識: 「自分で書く」は理解だけでなく 運用 の質を上げます。例えばブラウザだと再現できないバグも、curl なら同じヘッダを送り続けてサーバの挙動を切り分けられます。プロのサポートエンジニアは ブラウザより先に curl を開く と言われるほどです。

確認: ブラウザで再現できないが curl なら再現できるバグがあるとする。最もあり得るシナリオは?

正解:B。ブラウザは多数のヘッダを自動付与し、CORS や cache 制御も裏で実施する。curl は最小限の HTTP しか送らないので、サーバが「特定のヘッダがあること」「特定のオリジンから来ること」を前提に挙動を変えていると、curl の結果は別物になる。これを使えば「サーバはどのヘッダで挙動を変えているか」を切り分け解析できる ─ プロのデバッグツールとして curl が選ばれる本質的理由。

生 HTTP を手で打つ ─ telnet / nc

HTTP/1.x はテキストベースのプロトコルなので、TCP の上に正しい順序で文字列を流せば サーバは応答してくれます。これを実演する最小の道具が telnet(または nc / netcat) です。普段はブラウザが裏でやっている処理を、自分の手でなぞってみます。
事前準備: Windows 10 / 11 では telnet クライアントが既定で無効化されており、macOS でも High Sierra(10.13)以降は標準コマンドから外されています。手元に telnet が無い場合は nc(netcat)、Windows なら PuTTY の Raw モード、HTTPS まで含めるなら openssl s_client -crlf で代用できます。
POINT 手で打つときの3つの掟:
① 行末は必ず CRLF(\r\n)。LF(\n) だけだと多くのサーバが受け付けない
② ヘッダの最後に 空行(CRLF だけの行) を必ず入れる。これが「ヘッダ終わり」の合図
③ HTTP/1.1 は Host ヘッダが必須。書かないと 400 Bad Request になる

walkthrough:telnet で example.com に GET を打つ6ステップ

STEP 1 ── サーバに TCP 接続する

$ telnet example.com 80
Trying 23.215.0.136...
Connected to example.com.
Escape character is '^]'.

telnet は 引数で指定したホスト・ポート に TCP 接続するだけのツール。ここまでで TCP の 3-way ハンドシェイクが完了しています。下層は 標準編 TCP 参照。

STEP 2 ── リクエスト行を打つ

GET / HTTP/1.1

「メソッド SP URI SP HTTP-バージョン」。Enter キーを押すと CRLF が送信されます(端末によっては LF だけの場合があるので、後述の nc -C のような工夫が必要)。

STEP 3 ── ヘッダ行を1行打つ

Host: example.com

HTTP/1.1 で必須の Host ヘッダ。同じ IP に複数のサイトが同居している前提なので、これを書かないと「どのサイトの / を要求しているのか」サーバに伝わりません。

STEP 4 ── 空行を打つ(超重要)

(ここで Enter だけを押す = CRLF だけの行)

これが「ヘッダ終わり、本文に入ります」という合図です。GET には本文がないので、空行の直後にサーバが処理を開始します。

STEP 5 ── レスポンスを受け取る

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 の数だけ本文を読めばよい、というのがクライアントの基本動作です。

STEP 6 ── コネクションを切る

Connection closed by foreign host.

HTTP/1.1 はパーシステント接続がデフォルトですが、Connection: close を付けるか、サーバのアイドルタイムアウトを過ぎるとサーバ側から切ってきます。telnet は ^](Ctrl+]) → quit でも切れます。

送信メッセージの可視化

telnet で打つ生バイト列(改行は CRLF = \r\n) GET / HTTP/1.1\r\n ← リクエスト行 Host: example.com\r\n ← ヘッダ行(必須) User-Agent: handcraft/1.0\r\n ← ヘッダ行(任意) \r\n ← 空行(これがヘッダ終わりの合図!) (本文なし。GET なので空)

図の見方:HTTP メッセージは「行」がすべて CRLF で終わるテキストの並び。空行(\r\n だけの行) が「ここでヘッダが終わる」を示すデリミタになっている。空行を忘れるとサーバはずっと「もっとヘッダが来るかも」と待ち続け、応答が返ってこない。

Q. もし行末を LF(\n) だけ で送ったらどうなるでしょう? なぜ CRLF(\r\n) が必須なのか?

実環境では多くのサイトが HTTPS のみ(80 番は 301 で 443 にリダイレクト) です。telnet は平文しか喋れないので、TLS 越しの HTTPS には接続できません。HTTPS で生 HTTP を打ちたいときは openssl s_client -connect example.com:443 を使うと、TLS ハンドシェイク後の平文セッションを開けます(詳細は TLS/PKI 標準編)。

curl ─ telnet の一段上、Python の一段下(紹介)

curl は HTTP プログラミングの定番 CLI ツールです(macOS / Linux / Windows 10 以降に概ね標準搭載)。telnet が「全部手打ち」だったのに対し、curl は 面倒な部分(TCP 接続、TLS、Host ヘッダ、Content-Length 計算など) を自動でやった上で、必要な部分だけ引数で上書き できます。本講では「あるツール」として前提し、各節で必要に応じて使っていきます。
導入: macOS / Linux は標準で同梱、Windows 10 以降も標準コマンドとして付属しています。curl --version で確認できます。主要オプション -v(verbose) -H(ヘッダ追加) -X(メソッド) -d(本文) -c -b(Cookie) -o -i(出力)を組み合わせれば GET / POST / Cookie / JSON / multipart など実用ケースの大半をカバーできます。HTTPie のような「人にやさしい curl」 も同じ思想の派生ツールです。

触りだけ:1行で example.com を取る

# 本文だけ標準出力
$ curl https://example.com/

# -v(verbose) で送信ヘッダ・受信ヘッダ・TLS の様子を全部見る
$ curl -v https://example.com/

telnet で十数行手打ちしていた処理が、curl では 1行-v を付ければ「自分が何を送ったか(>)」「サーバが何を返したか(<)」が両方見えるので、telnet 手打ちの代わりに HTTP の挙動を観察 する用途にも使えます。

4ツールで「同じ GET を打つ」比較

初学者がよく混乱する 「同じことをやるのに書き方がツールごとに違う」 問題。example.com に GET を打つ という同じ動作を、4つのツールで並べてみます。

telnet(生バイト)

$ telnet example.com 80
GET / HTTP/1.1
Host: example.com

(空行 ← ここで Enter)

最も低レベル。CRLF・空行・Host ヘッダを 全部自分で 用意する必要がある。

curl(CLI)

$ curl http://example.com/

# verbose 表示で何が起きているかを見る
$ curl -v http://example.com/

1行で済む。Host ヘッダ・User-Agent・Accept は curl が自動で付ける。シェルスクリプトに組み込みやすい

Python (requests)

import requests

r = requests.get("http://example.com/")
print(r.status_code)        # 200
print(r.headers["Content-Type"])
print(r.text[:200])

Python 側からアクセスして 結果をプログラムで処理 したいときの定番。レスポンスがオブジェクトとして扱える。

Node (fetch)

// 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 はデバッグ目的の悪手(証明書問題を隠す)。

Python クライアント:http.client と requests

Python で HTTP を喋る方法は大きく2つ。標準ライブラリの http.client(あるいは urllib.request) と、デファクトのサードパーティ requests。仕事でも学習でも使うのは圧倒的に後者ですが、「中で何が起きているか」を知るには前者が役立ちます。

(a) http.client ─ 標準ライブラリ

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 変換・接続再利用は 自分で書く必要 があります。

(b) requests ─ 圧倒的に使われるサードパーティ

インストール: 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())

(c) Session でコネクション再利用 + Cookie 自動管理

同じサイトに複数回アクセスするなら 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}")
もっと詳しく:requests.Session の中で何が起きているのか

裏側はけっこう複雑なことをやっています。だからこそ「Session を使わずにグローバル requests.get をループで回す」と、毎回 TCP/TLS 確立で大幅に遅くなるという初学者の罠が生まれます。

(d) エラー処理の定石

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) のようにタプルで分けて指定できます。タイムアウト未指定だと無限に待つ ので本番コードでは必ず付けましょう。

小問:HTTP リクエストで Content-Length ヘッダの値を 本文の実際のバイト数より小さく 設定して送ったら、サーバ側ではどう解釈されるか?
正解:A。サーバは Content-Length の値を そのまま信じて その分だけ本文として読みます。実本文の方が長ければ、残りは 次のリクエストの先頭 として解釈され、メッセージ境界が壊れます。これは HTTP リクエストスマグリング(Request Smuggling) という有名な攻撃の入口にもなる挙動で、proxy / load balancer 跨ぎで深刻な問題を起こします。逆に Content-Length の方が大きければ、サーバは本文の続きを永久に待ってタイムアウトします。本文の長さは絶対に正確に ── これが HTTP/1.1 の鉄則です。

発展:aiohttp で並列リクエスト

数百〜数千の URL に同時にアクセスしたい場合(クローラ・ヘルスチェッカ・スクレイパなど) は、同期版 requests を for ループで回すと 1件ごとに RTT 分待つ ため非効率です。Python の非同期 HTTP クライアント aiohttp を使うと、1スレッド内で何千本ものリクエストを並行 させられます。
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))
POINT 同期版 requests: URL × 各 RTT(逐次)
非同期版 aiohttp: max(各 RTT) ≒ 1 RTT(並行)
100 URL × 200ms RTT なら、20 秒 → 0.5 秒 程度に短縮。ただし対象サーバへの負荷を考えて 同時実行数の上限(セマフォ) を入れるのがマナー。
同期版で書きやすい requests 互換 API + 非同期両対応の httpx も近年人気です(pip install httpx)。HTTP/2 にも対応しており、新規プロジェクトでは選択肢に入れる価値があります。[要確認] 機能比較の細部は公式ドキュメント参照。

最小限の HTTP サーバを起動する

クライアントだけでなくサーバも書けると、HTTP の理解が一段階深まります。Python には 標準ライブラリだけで動くサーバ が3パターン用意されています。

(a) 静的ファイル配信:1コマンドで動く

# カレントディレクトリを 8000 番ポートで配信
$ python -m http.server 8000

# 別端末から確認
$ curl http://localhost:8000/
$ curl http://localhost:8000/index.html

HTML / JS / 画像をローカルで動作確認したい時の定番。ディレクトリリスティング も自動で出してくれます(本番環境ではセキュリティ的に NG)。

(b) BaseHTTPRequestHandler:動的応答

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_GETdo_POST をメソッドで定義すれば、メソッドごとの処理がそのままマッピングされます。end_headers空行(CRLF) を送る点に注目 ── 前のセクションで強調した「ヘッダ終わりの合図」と一致しています。

(c) Flask / FastAPI ─ 実用的なフレームワーク

業務での Web アプリ・API は フレームワーク を使います。標準ライブラリ版より圧倒的に書きやすく、ルーティング・バリデーション・テストも整っています。

Flask(同期)

# 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)

シンプル・歴史も長い。学習・小規模アプリの定番。

FastAPI(非同期、型ヒント)

# 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 ドキュメント生成。非同期サポート。新規プロジェクトでの人気が高い。

Node.js Express(参考)

// 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 圏のデファクト。ミドルウェア合成型の設計で、巨大なエコシステムを持つ。

もっと詳しく:Flask vs FastAPI vs Express 同じ Hello World の違い

3つとも「/hello に GET したら JSON を返す」だけのコードですが、設計思想に違いがあります。

選ぶ基準は 言語・性能要件・開発規模・チームの慣れ。プロトタイプは Flask、本番 API は FastAPI、フロントエンドと同居なら Express、というのが2026年時点の典型的な棲み分けです。[要確認] 最新版番号や微細な API 差は各公式ドキュメント参照。

Node.js で書く ─ http モジュールと fetch

Node.js は JavaScript ランタイムで、サーバサイド JavaScript の中心。HTTP 関連は標準モジュール http / https と、Node.js 18 以降で組み込まれた fetch が主役です。

(a) http.createServer ─ 最小サーバ

// 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 で応答を送ります。

(b) クライアント:組み込み fetch を使う(Node.js 18+)

// 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 パッケージを使う必要がありました。

(c) より低レベル:http.request

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 は怖くなくなります。

デバッグ手段の使い分け

HTTP のトラブルは多層に渡るため、道具を使い分け て切り分けます。「どの層で問題が起きているか」によって最適なツールが違います。
道具見える層得意なシーン
curl -vHTTP/1.1 のリクエスト・レスポンス全体ヘッダの値・ステータスコードを正確に確認したい時。最初の道具
Chrome DevTools の Network タブブラウザが投げた HTTP すべてブラウザ操作で再現する不具合の調査。タイミング・キャッシュ・Cookie が見やすい
HTTPie / PostmanHTTP/1.1 のメッセージAPI の対話的探索、ドキュメント例の試行
WiresharkTCP/IP のパケット全部「そもそも TCP がつながってない」「TLS でコケてる」を切り分け
tcpdump同上(CLI 版)サーバ側でリモートにキャプチャ。後で Wireshark で開く
mitmproxyHTTPS 含む HTTP すべてクライアント↔サーバ間に挟まり、TLS を解いて生 HTTP を見る・改変する
サーバアクセスログサーバが受けたリクエスト「リクエストが届いていない」のか「届いているが応答がおかしい」かの一次切り分け

典型的な切り分けフロー

問題発生 「API が動かない」 curl -v で同じ要求 → 同じエラー? ブラウザだけ? サーバアクセスログ確認 → そもそも届いている? 届いていなければ tcpdump → TCP / TLS / DNS で詰まってる? 中継機器を mitmproxy で見る → 改変・喪失してないか 原因特定 → 修正 クライアント / サーバ / 経路 層を1つずつ下に降りて切り分け、原因が見えたら戻って修正する

図の見方:アプリ層(curl)で問題が見えるか確認 → 見えなければサーバログ → 見えなければ TCP/IP 層 → 見えなければ中継機器、と 層を下から確認していく のが原則。やみくもに上の層で再現を試すより、下に降りた方が問題に近づくことが多い。

もっと詳しく:mitmproxy で生 HTTP を覗く

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 越しの中身を解読 してリクエストとレスポンスを表示します。デバッグには強力ですが、他人の通信に対して使うのは違法・倫理違反 です。必ず自分の権限がある通信だけに使ってください。

まとめと用語チェック

SUMMARY 1. HTTP/1.x はテキストプロトコル。telnet / nc で 手で打って 動きを観察できる
2. CRLF と空行と Host ヘッダ ── これらが揃わないと応答が返らない
3. curl は telnet の一段上、Python の一段下。CLI でメソッド・ヘッダ・Cookie を制御する定番
4. Python は requests、特に Session でコネクション再利用と Cookie 自動管理
5. 高並列なら aiohttp / httpx。逐次→並行で大幅な高速化
6. サーバは python -m http.serverBaseHTTPRequestHandler で標準ライブラリだけで起動可。本番は Flask / FastAPI / Express
7. Node.js は組み込み fetch(18+) と http.createServer でクライアント・サーバ両方を簡潔に書ける
8. Cookie + セッション、Bearer トークンは HTTP 上の設計判断。仕様としては単なるヘッダ
9. デバッグは 層を下に降りて 切り分け:curl -v → サーバログ → tcpdump → mitmproxy

用語チェック

用語1行説明
telnet / ncTCP に直接接続して生テキストを送る最小ツール。HTTP の学習用に最適
CRLF行末コード \r\n。HTTP/1.x の行区切りで必須
curlHTTP プログラミングの定番 CLI。Cookie / 認証ヘッダ / multipart まで対応
http.clientPython 標準ライブラリの低レベル HTTP クライアント
requestsPython のデファクト HTTP クライアント。直感的 API
Sessionrequests のセッションオブジェクト。TCP 再利用 + Cookie 自動管理
aiohttp / httpx非同期 HTTP クライアント。高並列に向く
python -m http.server1コマンドで静的ファイル配信サーバを起動
BaseHTTPRequestHandlerPython 標準の動的応答サーバ基底クラス
Flask / FastAPIPython の主要 Web フレームワーク(同期/非同期)
http.createServerNode.js の最小 HTTP サーバ API
fetchブラウザ・Node.js 18+ の標準 HTTP クライアント API
Bearer トークンAuthorization ヘッダで運ぶ認証情報。SPA / API で頻出
mitmproxyHTTPS を解いて中身を見る・改変する中間プロキシ
関連回: プロトコル層の HTTP/3 / QUIC の実装は 第33回 QUIC と HTTP/3 をどうぞ。本講と 第28回 ネットワークプログラミング第30回 Wireshark をセットで読むと、「HTTP を喋る側」「プロセスとしてのサーバ」「ワイヤを流れるパケット」 が一本に繋がります。

確認問題

問1. telnet で生 HTTP リクエストを打つとき、必ず必要なものとして最も適切なものを1つ選べ。

次の選択肢から最も適切なものを選択してください。
A. リクエスト行のあとに Content-Length ヘッダを必ず1個入れる
B. リクエスト行と本文の間に必ずバイナリのフレームヘッダを入れる
C. ヘッダの最後に空行(CRLF だけの行) を入れて「ヘッダ終わり」を示す
D. 必ず最初に SETTINGS フレームを送る
正解:C
HTTP/1.1 では ヘッダの最後に CRLF だけの空行 を入れることでヘッダ終わりを示す。これがないとサーバはヘッダがまだ続くと思って待ち続ける。A は GET など本文がないリクエストでは不要、B・D は HTTP/2 の話で 1.1 とは関係ない。

問2. telnet と curl の使い分けに関する記述として最も適切なものを選べ。

次の選択肢から最も適切なものを選択してください。
A. curl は HTTP/1.1 専用で、HTTP/2 や HTTPS には対応していない
B. telnet は TLS ハンドシェイクを自動でやってくれるので HTTPS のサーバにそのまま使える
C. telnet は CRLF・空行・Host ヘッダを 全部自分で 用意する必要があるが、curl は TCP 接続・TLS・必須ヘッダを 自動で 整えてくれる
D. curl は GUI ツールなのでスクリプトに組み込みにくい
正解:C
curl は「面倒な前処理を自動化しつつ、必要な部分だけ引数で上書き」できる中間レベルのツール。HTTP/2 / HTTP/3 / HTTPS にも対応(A 誤り)、telnet は 平文 TCP しか喋れない ので HTTPS には使えない(B 誤り)、curl は CLI なのでシェルスクリプトに組み込みやすい(D 誤り)。

問3. requests.Session を使う主な利点として最も適切なものを選べ。

次の選択肢から最も適切なものを選択してください。
A. 同じホストへの TCP/TLS 接続を再利用し、Cookie を自動的にまたいで運ぶ
B. リクエストを HTTP/3 に自動アップグレードする
C. レスポンスを自動的にディスクに保存する
D. 認証情報を絶対に外部に出さないように暗号化する
正解:A
Session は内部にコネクションプール(urllib3) と Cookie jar を持ち、同じホストへの2回目以降のリクエストで TCP/TLS 確立をスキップ + Cookie を自動付与する。これにより HTTP/1.1 のパーシステント接続の恩恵を受けやすい。B(HTTP/3 化はしない)、C(自動保存はしない)、D(暗号化は TLS 側の話) はいずれも誤り。

問4. Content-Length ヘッダに関する記述として最も適切なものを選べ。

次の選択肢から最も適切なものを選択してください。
A. 値はクライアントが省略しても、サーバが自動で正しく計算してくれる
B. 実際の本文より大きな値を指定すると、サーバが自動で切り詰める
C. 実際の本文より小さな値を指定しても、メッセージ境界には影響しない
D. 値を間違えると、メッセージ境界が壊れて「次のリクエスト」が誤読される等の問題が起きる
正解:D
Content-Length はサーバが「本文をどこまで読むか」を決める基準。値が 本文より小さい と残りが次リクエストの先頭として誤解釈され(HTTP リクエストスマグリングの素地)、本文より大きい とサーバは続きを永久に待ってタイムアウトする。クライアントが正確に付与する責任を持つ(高レベルライブラリは自動でやってくれる)。

問5. Cookie ベース認証と Bearer トークン認証の違いに関する記述として最も適切なものを選べ。

次の選択肢から最も適切なものを選択してください。
A. Cookie はブラウザが自動で送るが、Bearer トークンはサーバが自動でセットする
B. Cookie はブラウザがドメイン単位で自動管理し、Bearer トークンはクライアントが Authorization ヘッダで毎回明示的に付与する
C. Bearer トークンは HTTP/1.1 では使えず、HTTP/2 専用の機能である
D. Cookie と Bearer はどちらも TLS 上でしか使えない仕様になっている
正解:B
Cookie はブラウザがドメイン・パス・属性に従って自動で送るのが特徴で、Web アプリのログイン維持に向く。Bearer トークンはクライアントが Authorization: Bearer ... を明示的に付ける形で、SPA / モバイル / 他オリジン API に向く。A・C・D はいずれも誤り(C: バージョン非依存、D: 仕様上は HTTP でも使えるがセキュリティ上 HTTPS が前提)。
← PREV
第28回 ネットワークプログラミング
NEXT →
第30回 Wireshark でパケットを観る