HTTP入門:curl -v の出力を1行ずつ読む

HTTP講座1。python http.server をローカルで起動し、curl -v を1行ずつ読む。メソッド、ステータスコード、ヘッダー、curl の終了コード、実測した通信時間まで。

シリーズ:HTTP講座 1/6(初級)。 API を毎日使っているのに、生の HTTP メッセージを見たことがない新人エンジニア向けです。読み終えると、コマンド1つで Web サーバーを起動し、curl -v の出力を全行読めて、メソッド・ステータスコード・ヘッダーが何なのかを説明できるようになります。

「アプリからサーバーにつながりません」と相談されたとき、新人が見られるのはぐるぐる回るインジケーターだけ、ということがよくあります。この講座では、その当て推量を終わらせる道具として、自分で立てたサーバーに curl -v を打ち、手元から出ていったリクエストと返ってきたレスポンスをそのまま見る方法を身につけます。以下はすべて Apple Silicon Mac(M1 Pro、macOS 26.6)の curl 8.7.1 と Python 3.9.6 で実行した結果で、サーバーは 127.0.0.1 で動かし、ファイルパスは ~/demo/ に置き換えています。

サーバー側の言語の知識は要りません。ターミナルで cd が打てれば最後までついて来られます。

URL を叩くと何が起きるか

HTTP(HyperText Transfer Protocol)は リクエスト/レスポンス型 のプロトコルです。クライアント(ブラウザ、アプリ、curl)が サーバー に接続し、テキストの行でできたリクエストを送ると、サーバーはテキストの行と本文(ボディ)でできたレスポンスを返します。その後、接続は閉じられるか、次のリクエストに再利用されます。

http://127.0.0.1:18101/data.json のような URL は、毎日使う4つの部品でできています。

  • スキーム http。https は同じ HTTP を暗号化した TLS 接続の中で送るもの(第5回)。
  • ホスト 127.0.0.1。「このパソコン自身」を指すループバックアドレス。
  • ポート 18101。省略すると http は 80、https は 443。
  • パス /data.json。? の後ろに クエリ文字列 が付くこともあります。
2段の流れ図。上段:curl が URL を受け取り、ホスト名を IP アドレスに解決し、ポートへ TCP 接続を開き、GET / HTTP/1.1 や Host など > で始まるリクエスト行を送る。下段:サーバーはリクエストを読んでログを1行書き、< で始まるレスポンス行(HTTP/1.0 200 OK などのステータス行、ヘッダー、空行、本文)を返し、最後に接続を閉じる。
curl から送る HTTP リクエスト1回の流れ。どの箱も curl -v が1行以上表示する段階で、サーバー側の動きはサーバーのログに出る。

5分で最初のサーバーを立てる

フォルダーにファイルを2つ作り、Python 付属のファイルサーバーを起動します。空いているポートなら何番でも構いません。ここでは 18101 を使いました。

mkdir -p ~/demo && cd ~/demo
printf '<!doctype html>\n<html><head><meta charset="utf-8"><title>Hello</title></head>\n<body><h1>Hello, HTTP</h1></body></html>\n' > index.html
echo '{"name":"demo","ok":true}' > data.json
python3 -m http.server 18101 --bind 127.0.0.1

--bind 127.0.0.1 は省略しないでください。付けないと全ネットワークインターフェースで待ち受けるので、社内や自宅の同じ Wi-Fi にいる他の端末からもフォルダーの中身が読めてしまいます。このターミナルは開いたままにし(ここにログが出ます)、curl 用にもう1つターミナルを開きます。終了は Ctrl+C です。

curl -v を1行ずつ読む

curl -sv http://127.0.0.1:18101/

-v(verbose)がやり取りを表示し、-s(silent)はパイプしたときに混ざる進捗表示を消します。実際の出力です。

*   Trying 127.0.0.1:18101...
* Connected to 127.0.0.1 (127.0.0.1) port 18101
> GET / HTTP/1.1
> Host: 127.0.0.1:18101
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.9.6
< Date: Thu, 12 Nov 2026 14:33:47 GMT
< Content-type: text/html
< Content-Length: 119
< Last-Modified: Thu, 12 Nov 2026 14:28:18 GMT
< 
{ [119 bytes data]
* Closing connection
<!doctype html>
<html><head><meta charset="utf-8"><title>Hello</title></head>
<body><h1>Hello, HTTP</h1></body></html>

行頭の記号は3種類です。

  • * は curl からあなたへのメモ。どのアドレスに接続を試みたか、TCP 接続がいつ確立し、いつ閉じたか。
  • > は curl が送った リクエスト。1行目が リクエスト行 で、メソッド GET、パス /、プロトコルのバージョン HTTP/1.1 が並びます。続いて 名前: 値 形式の リクエストヘッダー。HTTP/1.1 では Host が必須です。1つの IP アドレスで複数のサイトを動かせるからです。空の > 行でヘッダーが終わり、GET には本文がないのでリクエストはここまで。改行コード込みでちょうど 78 バイトでした。
  • < は レスポンス。ステータス行 HTTP/1.0 200 OK にバージョン、ステータスコード 200、人間向けの説明が入ります。次に レスポンスヘッダー、空行、そして 本文(119 バイト。Content-Length: 119 と一致)。

バージョンの食い違いにも注目してください。curl は HTTP/1.1 で聞き、Python のサーバーは HTTP/1.0 で答え、curl は HTTP 1.0, assume close after body とメモしています。サーバーは低いバージョンで答えてよく、HTTP/1.0 ではレスポンスごとに接続が閉じます。この話は第6回(keep-alive)でまた出てきます。

サーバー側のターミナルにはリクエストごとに1行のログが出ます。GET、HEAD、存在しないファイル、POST の4回分です。

127.0.0.1 - - [12/Nov/2026 14:33:47] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [12/Nov/2026 14:33:48] "HEAD /data.json HTTP/1.1" 200 -
127.0.0.1 - - [12/Nov/2026 14:33:48] code 404, message File not found
127.0.0.1 - - [12/Nov/2026 14:33:48] "GET /nope.html HTTP/1.1" 404 -
127.0.0.1 - - [12/Nov/2026 14:33:48] code 501, message Unsupported method ('POST')
127.0.0.1 - - [12/Nov/2026 14:33:48] "POST / HTTP/1.1" 501 -

クライアントの IP、時刻、引用符の中のリクエスト行、ステータスコード。Apache や nginx のアクセスログと同じ形です。クライアント側とサーバー側を並べて読むのが、いちばん速い原因の切り分け方です。(Date ヘッダーは常に GMT なので、ログの時刻と揃えるためにサーバーは TZ=UTC で起動しました。)

メソッド:サーバーに何を頼むのか

メソッド はリクエスト行の動詞です。

メソッド 意味 本文 繰り返しても安全?
GET リソースを読む なし はい
HEAD GET と同じだがヘッダーだけ なし はい
POST データを送る・何かを作る あり いいえ
PUT リソースを置き換える あり はい(冪等)
PATCH リソースの一部を変える あり 保証なし
DELETE リソースを消す 通常なし はい(冪等)

「冪等(べきとう)」は、同じリクエストを2回送っても1回送ったのと結果が同じ、という意味です。アプリが GET は気軽にリトライし、POST のリトライには慎重になる(注文が2件できてしまう)のはこのためです。

今回のサーバーは GET と HEAD しか実装していません。curl -I は HEAD を送ります。

$ curl -sI http://127.0.0.1:18101/data.json
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.9.6
Date: Thu, 12 Nov 2026 14:28:25 GMT
Content-type: application/json
Content-Length: 26
Last-Modified: Thu, 12 Nov 2026 14:28:18 GMT

POST を送ると(-d で本文を付けるとメソッドは自動的に POST になります)こうなります。

$ curl -si -d 'a=1' http://127.0.0.1:18101/ | head -1
HTTP/1.0 501 Unsupported method ('POST')

501 は「そのメソッドは実装していない」という意味で、ファイルサーバーとしては正直な答えです。第2回では POST を受け付ける小さな php -S ルーターを書きます。

ステータスコードとヘッダーをざっと見る

ステータスコードは先頭の数字で分類されます。2xx 成功、3xx 別の場所へ、4xx クライアント側の誤り、5xx サーバー側の失敗。詳しくは第2回で全部動かします。ここでは存在しないファイルへの 404 を読んでみます。

$ curl -si http://127.0.0.1:18101/nope.html | head -6
HTTP/1.0 404 File not found
Server: SimpleHTTP/0.6 Python/3.9.6
Date: Thu, 12 Nov 2026 14:28:25 GMT
Connection: close
Content-Type: text/html;charset=utf-8
Content-Length: 469

説明文は自由で、一般的な "Not Found" ではなく "File not found" でした。プログラムが見てよいのは数字だけです。

よく読むヘッダーは次のとおりです。

  • Content-Type:本文の種類(text/html、application/json)。Python は拡張子から推測しています。200 のページは charset なし の text/html、エラーページは charset=utf-8 付きでした。charset がない・間違っていると何が起きるか(文字化け)は第3回で実演します。
  • Content-Length:本文のバイト数。
  • Date、Last-Modified:常に GMT。Last-Modified はファイルの更新時刻で、キャッシュに使われます(第4回)。
  • Server:ソフトウェア名。本番ではバージョンを隠すことが多いです。
  • Location:次に行く先。3xx と一緒に使います。

末尾のスラッシュなしでフォルダーを指定すると、Location が実際に出てきます。

$ curl -si http://127.0.0.1:18101/docs | head -5
HTTP/1.0 301 Moved Permanently
Server: SimpleHTTP/0.6 Python/3.9.6
Date: Thu, 12 Nov 2026 14:28:42 GMT
Location: /docs/
Content-Length: 0

curl は -L を付けない限り追いかけません。-L と -w '%{http_code} %{num_redirects} %{url_effective}' を付けると 200 1 http://127.0.0.1:18101/docs/ と表示されました。

HTTP を手で打ってみる

HTTP/1.x はただのテキストなので、curl なしでも送れます。macOS には nc(netcat)が入っています。

$ printf 'GET /data.json HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n' | nc -w 2 127.0.0.1 18101
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.9.6
Date: Thu, 12 Nov 2026 14:28:42 GMT
Content-type: application/json
Content-Length: 26
Last-Modified: Thu, 12 Nov 2026 14:28:18 GMT

{"name":"demo","ok":true}

改行は \r\n、空行でヘッダーの終わりです。同じサーバーで意外な結果が2つありました。

$ printf 'GET /data.json\r\n\r\n' | nc -w 2 127.0.0.1 18101
{"name":"demo","ok":true}

ステータス行もヘッダーもありません。バージョンのないリクエスト行は HTTP/0.9(1991年)として扱われ、当時のレスポンスは本文だけでした。ログは "GET /data.json" 200 - です。HTTP ですらないものを送ると、こうなります。

$ printf 'HELLO\r\n\r\n' | nc -w 2 127.0.0.1 18101 | grep -E "Error code|Message"
        <p>Error code: 400</p>
        <p>Message: Bad request syntax ('HELLO').</p>

よくある間違いと curl の終了コード

curl の 終了コード(直後に echo $?)を見ると、どの層で失敗したかが分かります。実際の実行結果です。

$ curl -sS http://127.0.0.1:18149/
curl: (7) Failed to connect to 127.0.0.1 port 18149 after 0 ms: Couldn't connect to server
$ curl -sS "http://127.0.0.1:18101/my file.txt"
curl: (3) URL rejected: Malformed input to a URL function
$ curl -sS htp://127.0.0.1:18101/
curl: (1) Protocol "htp" not supported
$ curl -fsS http://127.0.0.1:18101/nope.html
curl: (22) The requested URL returned error: 404
$ curl -s http://127.0.0.1:18101/nope.html -o /dev/null; echo "exit=$?"
exit=0

間違い:「curl が成功したから API は動いている」。 実際には最後の例のとおり、404 でも終了コードは 0 です。curl にとってはエラーページも「ダウンロード成功」です。対策: シェルスクリプトや CI では curl -fsS(4xx/5xx で終了コード 22、静かだがエラーは表示)を使うか、-w '%{http_code}' でコードを出して確認します。

間違い:URL に空白を入れる。 curl 8.7.1 は終了コード 3 で拒否しました。%20 にすれば spaced が返り、ログは GET /my%20file.txt でした。クエリの値は curl にエンコードさせられます。curl -G --data-urlencode 'q=café & tea' … は GET /data.json?q=caf%c3%a9+%26+tea を送りました。

間違い:localhost と 127.0.0.1 は同じ。 ほぼ同じですが、localhost は ::1(IPv6)と 127.0.0.1 の両方に解決され、curl は IPv6 を先に試しました。

*   Trying [::1]:18101...
* connect to ::1 port 18101 from ::1 port 53480 failed: Connection refused
*   Trying 127.0.0.1:18101...

サーバーは 127.0.0.1 だけで待ち受けていたので1回目は失敗し、curl が IPv4 にフォールバックしました。動きはしますが、フォールバックしないクライアントだと、サーバーが動いているのに「接続拒否」になります。対策: 両側で同じアドレスを使います。

終了コード 意味 よくある原因
0 転送完了 -f なしなら 404/500 でも 0
1 未対応のプロトコル スキームの打ち間違い
3 URL が不正 空白、範囲外のポート番号
6 ホスト名を解決できない DNS 名の打ち間違い(今回は未実行)
7 接続できない サーバー停止、ポート違い、IPv4/IPv6 の違い
22 -f 付きで HTTP エラー 4xx・5xx
28 タイムアウト --max-time に到達(第2回)
47 リダイレクトが多すぎる リダイレクトループ(第2回)

知りたいことと curl のオプション対応表

知りたいこと コマンド
何を送って何を受け取ったか全部 curl -sv URL
ステータスとヘッダーだけ curl -sI URL(HEAD)または curl -si URL(GET、ヘッダー付き)
スクリプト用にステータスコードだけ curl -s -o /dev/null -w '%{http_code}\n' URL
4xx/5xx でスクリプトを止める curl -fsS URL
リダイレクトを追う curl -L URL
フォームのデータを送る curl -d 'a=1&b=2' URL
時間がどこにかかっているか curl -s -o /dev/null -w '%{time_connect} %{time_starttransfer} %{time_total}\n' URL

実測:リクエスト1回にどれくらいかかるか

-w は curl 自身が測ったタイマーを表示します。ローカルの Python サーバーに URL ごとに31回順番にリクエストし、1回目をウォームアップとして除いた中央値です(Apple M1 Pro)。

リクエスト ヘッダー 本文 接続完了 最初の1バイト 合計
/(index.html) 185 B 119 B 0.273 ms 0.733 ms 0.777 ms
/data.json 191 B 26 B 0.276 ms 0.749 ms 0.788 ms
/big.bin(1 MiB、シード固定の乱数) 204 B 1,048,576 B 0.269 ms 0.730 ms 1.304 ms
localhost 経由の / 185 B 119 B 0.331 ms 0.875 ms

ローカルでは 1 MiB 送っても増えたのは約 0.5 ms でした。localhost が約 0.1 ms 遅いのは、IPv6 の失敗した接続試行の分です。

実際のインターネットは別物です。https://example.com/(ヘッダー 291 バイト、本文 577 バイトの小さなページ)に5回リクエストした累積時間の中央値です。

https://example.com/ へのリクエストの累積時刻を示す棒グラフ。名前解決の完了が 3.2 ms、TCP 接続が 17.3 ms、TLS ハンドシェイクが 36.3 ms、レスポンスの最初の1バイトが 58.7 ms。
実際の HTTPS リクエストで時間がどこに使われるか。Apple M1 Pro の Mac から https://example.com/ へ curl -w を5回実行した中央値。値はリクエスト開始からの累積。127.0.0.1 の python http.server から同じくらい小さいページを取ると合計 0.78 ms だった。

合計約 59 ms のうち、サーバーが答えを作る時間はごく一部で、大半は TCP と TLS の往復待ちです。後の回で接続の再利用やリダイレクトの削減を気にするのはこのためです。数値はネットワーク環境で変わりますが、形はたいてい同じです。

正直な限界・バージョンについて

  • HTTP/1.1 は現在、RFC 9110(意味論:メソッド、ステータスコード)と RFC 9112(メッセージ形式)で定義されています。どちらも2022年6月発行で、RFC 7230〜7235 を置き換えました。HTTP/2 は RFC 9113、HTTP/3 は RFC 9114、PATCH は RFC 5789 です。
  • python3 -m http.server(3.9.6 で確認)は HTTP/1.0 で答え、GET と HEAD しか実装していません。開発用で、Python の公式ドキュメントも本番利用を勧めていません。新しい Python ではログやヘッダーが少し違う可能性がありますが、確認していません。
  • HTTP/0.9 の応答はこのバージョンの Python の動きです。最近のサーバーの多くはこうしたリクエストを拒否します。
  • 127.0.0.1 での計測は手元のマシンの速さで、ネットワークではありません。example.com の数値は、ある晩に1つの回線から取った5回分です。
  • ブラウザは実行していません。ブラウザは curl の3つよりずっと多くのヘッダー(Accept-Language、Cookie、Sec-Fetch-* など)を送ります。

演習

1. サーバーを起動せずに curl -sS http://127.0.0.1:18149/ を実行すると、終了コードはいくつで、何が分かりますか?

2. http://127.0.0.1:18101/nope.html のステータスコードだけを表示するコマンドと、それでシェルスクリプトを止めるコマンドを書いてください。

3. 上の curl -sv の出力で、サーバーが curl と違う HTTP バージョンで話したことを示す行はどれですか?

解答

1. 終了コード 7、Failed to connect … Couldn't connect to server。そのポートで誰も待ち受けていない(または ::1 だけなど別のアドレスで待ち受けている)ということです。接続の問題であって HTTP の問題ではなく、リクエストは1バイトも送られていません。

2. curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18101/nope.html で 404 と表示されます。スクリプトでは curl -fsS http://127.0.0.1:18101/nope.html || exit 1。curl は終了コード 22 で The requested URL returned error: 404 を表示します。

3. ステータス行の < HTTP/1.0 200 OK と、curl のメモ * HTTP 1.0, assume close after body です。リクエスト行は > GET / HTTP/1.1 でした。

次へ

よくある質問

curl -v の出力にある >、<、* は何ですか?

> で始まる行は curl が送ったリクエスト、< で始まる行はサーバーから返ってきたステータス行とヘッダー、* で始まる行は Trying 127.0.0.1:18101 や Connected to のような curl 自身のメモです。本文は記号なしで表示されます。

python -m http.server に POST すると 501 が返るのはなぜですか?

このサーバーのハンドラーは GET と HEAD しか実装していないからです。POST や DELETE には HTTP/1.0 501 Unsupported method ('POST') が返り、ログにも code 501 と出ました。開発用のファイルサーバーなので、POST を受けたいときは php -S やフレームワークを使います。

サーバーが 404 を返したら curl は失敗扱いになりますか?

なりません。オプションなしだと 404 でも終了コードは 0 です。HTTP のやり取り自体は成功しているからです。-f(--fail)を付けると終了コード 22 と The requested URL returned error: 404 が出るので、スクリプトではこちらを使います。

  • #HTTP
  • #初級
  • #curl
  • #http.server
  • #ステータスコード
  • #ヘッダー

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑