なぜこれが重要なのか:私たちはGeonodeというプロキシ販売業者であり、ユーザーがリンクやサイズ、利用可能状況などを安価に確認するために、常に当社経由でHEADリクエストを利用しているからです。 率直に申し上げて、HEADは単なる軽量なGETリクエストではなく、別のリクエストであるため、それをGETと同等に扱うと、確信を持って誤った結論を導き出してしまう恐れがあります。 HEADに対して200を返すURLでも、GETに対しては403を返す可能性があります。 HEADリクエストでContent-Lengthが表示されないリソースでも、GETリクエストでは表示される場合があります。また、ボット対策レイヤーは、通常とは異なるHEADリクエストを独自のシグナルとして扱う可能性があります。HEADリクエストはその本来の目的においては非常に優れていますが、「実際にこれを取得したらどうなるか」を推測するための指標としては不適切です。
正しい方法
curl -I https://example.com
curlのマニュアルでは、-I, --head
について次のように説明されています。「(HTTP FTP FILE) ヘッダーのみを取得します。 HTTPサーバーにはHEADコマンドがあり、curlはこのコマンドを使用してドキュメントのヘッダーのみを取得します。FTPやFILEのURLに対して使用した場合、curlはファイルサイズと最終更新日時のみを表示します。」
出力:
HTTP/2 200
content-type: text/html; charset=UTF-8
content-length: 1256
last-modified: Thu, 17 Oct 2019 07:18:26 GMT
cache-control: max-age=604800
これが見出しの質問に対する答えのすべてです。以下は、時間を節約するための部分です。
なぜ -X HEAD は間違っているのか
curl マニュアル の「-X, --request」の項で、この点について直接説明されています:
このオプションは、HTTP リクエストで使用される実際の文字列を変更するのみで、curl の動作そのものを変更するものではありません。たとえば、適切な HEAD リクエストを行いたい場合、
-X HEADを使用するだけでは不十分です。--headオプションを使用する必要があります。
その仕組み:-Xはメソッド文字列のみを置き換え、それ以外は何も変更しません。curlは依然としてGETを実行しているかのように振る舞い、つまりレスポンスボディを期待し続けます。 HEADを正しく実装しているサーバーは、ヘッダーのみを送信し、ボディは送信しません。curlは決して届かないコンテンツを待ち続け、タイムアウトまたは接続の切断によって処理が終了するまで、コマンドがハングしたように見えます。
マニュアルでは、リダイレクトによってユーザーを困惑させる-Xのもう一つの挙動についても警告しています。「--locationが使用されている場合、--requestで設定したメソッド文字列がすべてのリクエストに使用されます」。つまり、-X POST -Lと指定すると、リダイレクトチェーンのすべてのホップに対してPOSTが再送信されてしまいますが、これは通常、誰も意図しない動作です。
マニュアル自体にも記載されている一般的な原則は次の通りです。「通常、このオプションは必要ありません。あらゆる種類の GET、HEAD、POST、PUT リクエストは、専用のコマンドラインオプションを使用して呼び出すのが一般的です。」HEAD には -I、POST には -d、PUT には -T を使用し、-X は PROPFIND のような真に特殊なメソッドのために留保してください。
HEAD メソッドの正体
RFC 9110 の §9.3.2 では、次の一文で定義されています。
HEAD メソッドは GET メソッドと同一ですが、サーバーがレスポンスとしてコンテンツを送信してはならないという点だけが異なります。
また、その目的について次のように述べています。「HEADは、選択された表現に関するメタデータを、その表現データを転送することなく取得するために使用されます。多くの場合、ハイパーテキストリンクのテストや最近の変更点の確認を目的としています。」
これはサーバーに対する厳しい要件です — MUST NOT コンテンツを送信する — そして、これがcurlにコンテンツの受信を期待させるとcurlがハングアップしてしまう理由です。
ヘッダーに関するルールは意図的に緩く設定されており、これが人々が誤解しやすい部分です:
サーバーは、HEADリクエストに対するレスポンスにおいて、リクエストメソッドがGETであった場合に送信したであろうのと同じヘッダーフィールドを送信すべきである(SHOULD)。ただし、サーバーは、コンテンツの生成中にのみ値が決定されるヘッダーフィールドを省略してもよい(MAY)。
RFCには具体的な例が挙げられています。動的なレスポンスをバッファリングするサーバーは、GETリクエストに対して「HEADレスポンス内では生成されない」Content-LengthやVaryを生成する可能性があります。 RFCではこれらを「軽微な不整合」と呼び、「HEADリクエストは通常、効率性を目的として行われるため、HEADリクエストのためにコンテンツを生成して破棄するよりも望ましい」とみなしている。
したがって、HEADリクエストでContent-Lengthが欠落していることは、必ずしもバグであるとは限らず、また必ずしも意味があるわけでもありません。単に、ページをレンダリングして初めて判明するような処理を、サーバーが実行することを避けているだけかもしれません。
また、ツールを開発している場合は、リクエスト・ボディに関する知っておくべきルールもあります。HEADリクエスト内のコンテンツは「一般的に定義されたセマンティクスを持たず、リクエストの意味や対象を変更することはできず、リクエスト・スマグリング攻撃の可能性があるため、一部の実装ではリクエストを拒否し、接続を閉じる原因となる可能性があります」。 RFCでは、事前の具体的な取り決めがない限り、クライアントは「HEADリクエストにコンテンツを生成してはならない」と規定されています。HEADリクエストにはボディを送信しないでください。
HEADの活用シーン
実際に役立つケースをいくつか紹介します。いずれも、完全な転送を行う代わりに、数百バイトのデータのみを取得します。
URLが有効かどうかを確認する場合:
curl -sI -o /dev/null -w '%{response_code}\n' https://example.com
ダウンロード前にファイルのサイズを確認する場合:
curl -sI https://example.com/large.iso | grep -i content-length
リダイレクトチェーンを追跡・報告する場合:
curl -sIL -o /dev/null -w '%{num_redirects} hops -> %{url_effective}\n' https://example.com
サーバーが範囲指定リクエストに対応しているかを確認する場合(これにより、中断されたダウンロードを再開できるかどうかが決まります):
curl -sI https://example.com/file.zip | grep -i accept-ranges
ダウンロードせずに最新性を確認する場合:
curl -sI https://example.com/data.json | grep -iE 'last-modified|etag'
**一括リンクチェック。これは典型的な用途であり、帯域幅の節約効果が相乗的に高まります:
while read -r url; do
code=$(curl -sIL -o /dev/null -w '%{response_code}' --max-time 10 "$url")
echo "$code $url"
done < urls.txt
帯域幅が制限されている環境では、その節約効果は顕著です。URL 1件あたり500 KBを転送するリンクチェックが、代わりに数百バイトしか転送しないからです。1万件のURLを対象とした場合、その差は5ギガバイトと数メガバイトの差になります。
HEAD が誤解を招く場合
冒頭で注意を促した理由となる、失敗のパターンについて。
サーバーが HEAD を完全に拒否する場合。 GET が問題なく動作する URL に対して、405 Method Not Allowed
が発生します。静的コンテンツではあまり見られませんが、API やアプリケーションのエンドポイントでは珍しくありません。
サーバーが HEAD を異なる方法で処理する。 ステータスコードが異なる、ヘッダーが異なる、場合によってはアプリケーション内の処理経路が全く異なることもある。RFC ではコンテンツ由来のヘッダーを省略することが許可されており、実装によってその適用範囲は異なる。
キャッシュやCDNは、HEADリクエストを別個にキーとして扱う場合があります。 HEADリクエストのキャッシュヘッダーは、GETリクエストがヒットするキャッシュエントリとは異なるものを反映している可能性があるため、HEADリクエストはキャッシュの挙動を確認する信頼できる方法とは言えません。
ボット対策システムは異なる応答を返します。 未知のクライアントからの HEAD リクエスト自体がシグナルとなるため、得られる応答は、ブラウザからの GET リクエストが受け取るものとは異なる場合があります。
** ``Content-Length が存在しないか、誤っている場合があります。** 仕様上は許容されており、動的コンテンツでは一般的ですが、生成されたコンテンツのダウンロードサイズを推定する根拠としては不適切です。
リダイレクトの連鎖が異なる場合があります。 特にコンテンツネゴシエーションが関与する場合、GETとHEADを異なる場所にリダイレクトするサーバーもあります。
実際のリクエストがどのような動作をするかを知る必要がある場合は、実際のリクエストを実行し、ボディを無視してください:
curl -sS -o /dev/null -D - https://example.com
本物の GET リクエストで、ヘッダーは標準出力(stdout)に表示され、ボディは破棄されます。帯域幅のコストはかかりますが、正確な回答が得られます。2つの方法から慎重に選択してください:安さを重視する場合は -I
、正確さを重視する場合は -o /dev/null -D -
を使用します。
大規模なリソースに対しては、中間的な選択肢があります。リソース全体ではなく、1バイトだけをリクエストするのです:
curl -sS -r 0-0 -o /dev/null -D - https://example.com/large.iso
-r, --range
は「バイト範囲(つまり、ドキュメントの一部)」を取得するため、0-0
は最初の1バイトのみを取得します。これは、実質的な帯域幅コストをほとんどかけずに、本物のGETリクエストとして動作するものです。 マニュアルに記載されている注意点:「多くの HTTP/1.1 サーバーでは、この機能が有効になっていない」ため、まず Accept-Ranges: bytes
を確認し、それが存在しない場合は完全なレスポンスが返されることを想定してください。
スクリプトに取り入れたいパターン
上記のコマンドは、少し構造を整えるだけで、かなり使い勝手が良くなります。
正確な結果を報告するリンクチェッカー。単純なバージョンでは、200以外のステータスコードをすべてリンク切れとして扱ってしまうため、リダイレクトやHEADリクエストを拒否するサーバーに対して誤検知が発生してしまいます。以下のリンクチェッカーは、それらを区別しています:
check() {
local url="$1" code
code=$(curl -sIL -o /dev/null --max-time 10 -w '%{response_code}' "$url")
case "$code" in
200) echo "OK $url" ;;
405) code=$(curl -sSL -o /dev/null --max-time 10 -w '%{response_code}' "$url")
echo "GET:$code $url" ;;
000) echo "TIMEOUT $url" ;;
*) echo "$code $url" ;;
esac
}
405という分岐が重要です。HEADを拒否するサーバーはリンク切れではありませんし、それを確認するにはGETで再試行するしかありません。000は、curlが「HTTPレスポンスがまったく届かなかった」ことを報告する形式であり、これによりネットワーク障害とサーバーエラーを区別できます。
並列処理は慎重に。 リンクチェックは並列処理が容易に行えるため、処理を大規模に実行したくなる誘惑に駆られます。しかし、それは控えてください:
xargs -P 8 -I{} sh -c 'check "$1"' _ {} < urls.txt
8が妥当なデフォルト値です。ここでの上限は、自身の処理能力ではなく、他者のサーバーに過度な負荷をかけることへの配慮にあります。レート制限を引き起こすようなリンクチェッカーは、節約した分以上のコストを招くことになります。
必ずタイムアウトを設定してください。 応答のないホストに対する HEAD リクエストは、GET リクエストとまったく同じだけハングします。--max-time 10 に --connect-timeout 5 を指定することで制限が設けられ、数千の URL をループ処理する場合、この制限こそが処理を完了させる鍵となります。
ステータスだけでなく、実際のURLも記録してください。 -L の後に %{url_effective} を実行すると、リンクが実際にどこに到達したかがわかります。これにより、「このリンクは機能する」という結果が、「このリンクは機能するが、現在は別の場所を指している」という結果に変わります。通常、後者の方が興味深い発見となります。
結果をキャッシュしましょう。 実行のたびにすべてのURLを再チェックするのは、帯域幅と労力の無駄です。ステータスと ETag または Last-Modified を保存し、その後の処理では条件付きリクエストを使用することで、変更のないリソースに対しては完全なチェックではなく 304 のみを実行するようにします。
プロキシ経由での HEAD リクエスト
3つの点が変更されており、いずれも知っておく価値があります。
curl -I -x http://user:pass@proxy.example.com:9000 https://example.com
HTTPSの場合、最初にCONNECTが実行されます。 curlはHEADリクエストの前にトンネルを確立し、詳細出力では、それに対するプロキシの応答がターゲットの応答よりも先に表示されます。--suppress-connect-headersではこれが非表示になり、%{http_connect}ではプロキシのステータスがターゲットのステータスとは別に報告されます。
重要なのは帯域幅の節約です。 通信量制限のある環境では、HEADリクエストの消費量は数百バイト程度であるのに対し、ページ全体をリクエストするとそれ以上の帯域幅を消費します。リンクの検証、可用性の監視、および大量のサイズチェックにおいて、これは「費用対効果の高い作業」と「高額な作業」を分ける要因となります。 これは、ギガバイト単位の課金体系において、数少ない真に大きな最適化の一つです。
しかし、ブロックやチャレンジの挙動は異なります。 GETリクエストに対してチャレンジページを返すボット対策レイヤーは、HEADリクエストを単に拒否する場合もあれば、その逆の場合もあります。 HEADを使用してターゲットにアクセスできるかどうかを確認する場合は、リスト全体に対して信頼する前に、サンプルに対して実際のGETリクエストを実行して結論を検証してください。これは、プロキシのテストが重要な理由で取り上げた「サイレント・フェイル」のパターンです。リクエストは成功し、応答は間違っているにもかかわらず、そのことを知らせるものは何もないのです。
curl について知っておくべき細かい点として、-G, --get は --head と組み合わせて使用できます。マニュアルには、-G を --head と併用した場合、「POST データは HEAD リクエストの URL に追加される」と記載されています。これは、HEAD リクエストでキーと値のペアから構成されたクエリパラメータが必要な場合に役立ちます。
レスポンスを正しく読み取る
返ってきた情報を最大限に活用する。
まずステータスコードを確認する。 200
は存在します。301
/302
は移動しました — -L
を追加してアクセスしてください。403
は拒否されました。404
は存在しません。405
は、サーバーが HEAD を受け付けないことを意味するため、GET として再試行してください。429
は、処理速度を落とすことを意味します。
**Content-Length
** が存在する場合(仕様上、省略が許可されていることに留意)。
**Accept-Ranges: bytes
** は、ダウンロードの再開と範囲指定リクエストが利用可能であることを意味します。
**Last-Modified
および ETag
** は、条件付きリクエストを有効にします。-z
は If-Modified-Since
を送信します(マニュアルでは、「指定された日時以降に変更されたファイル」をリクエストするものとして説明されています)。また、--etag-compare
は ETag
側を処理します。 304 Not Modified
はコストがほぼかからず、リソースを繰り返しポーリングするための正しい方法です。
**Content-Type
** には、本来受け取れたはずの内容が表示されます。JSONを期待していた場所で text/html
が表示される場合は、通常、エラーまたはログインページを意味します。
機械による処理の場合は、テキストの解析を完全に省略してください:
curl -sI -o /dev/null -w '%{header_json}' https://example.com | jq
%{header_json}
は、すべてのレスポンスヘッダーを、小文字の名称と配列値を持つJSONとして出力します。これにより、重複するヘッダーが正しく処理され、パーサーが不要になります。これおよびその他の確認オプションについては、curl によるレスポンスヘッダーの表示で解説しています。
よくある質問
curl で HEAD リクエストを送信するにはどうすればよいですか?
curl -I https://example.com. 完全な形式は --head です。-X HEAD は使用しないでください。マニュアルには、curl がレスポンスボディを期待し続ける一方で、このオプションはメソッド文字列のみを変更するため、適切な HEAD リクエストには「不十分」であると明記されています。
なぜ curl -X HEAD は応答しなくなるのですか?
これは、-Xがリクエスト行の単語を変更するだけで、curlの動作そのものを変更しないためです。curlは依然としてレスポンス本体を待ち続けますが、RFC 9110ではHEADレスポンスにおいてサーバーが「コンテンツを送信してはならない(MUST NOT send content)」と規定されているため、サーバーは正しく何も送信しません。代わりに-Iを使用してください。
HEADとGETの違いは何ですか?
HEADは、サーバーがレスポンス本文を送信してはならない点を除いて、GETと全く同じです。これは、コンテンツを転送せずにメタデータを取得するために存在し、通常はリンクの確認や最新性の確認に使用されます。サーバーはGETの場合と同じヘッダーを送信すべきですが、「コンテンツの生成中にのみ算出される」ヘッダーのみ省略してもかまいません。
HEADは常にGETと同じヘッダーを返すのでしょうか?
いいえ。仕様では、サーバーは同じヘッダーを送信「すべき」ですが、「コンテンツの生成中にのみ値が決定される」ヘッダーは省略「してもよい」とされています。例として、Content-LengthやVaryが挙げられています。仕様では、こうした些細な不整合は、ボディを生成して破棄するよりも望ましいとされています。
ファイルをダウンロードせずにファイルサイズを取得するにはどうすればよいですか?
curl -sI URL | grep -i content-length を使用します。ただし、動的に生成されたコンテンツの場合、このヘッダーが存在しない可能性があることに注意してください。これは仕様で許可されています。ほぼコストをかけずに、より信頼性の高い結果を得るには、-r 0-0 で 1 バイトをリクエストし、Content-Range ヘッダーを読み取ってください。
なぜブラウザではURLが機能するのに、curl -Iでは405が返ってくるのですか?
そのエンドポイントでサーバーがHEADを受け付けていないためです。GETを正常に処理するサーバーからのHEADリクエストに対して、405 Method Not Allowedは有効なレスポンスです。ボディを省略してGETとして再試行してください:curl -sS -o /dev/null -D - URL。
ボディを含む HEAD リクエストを送信できますか?
送信すべきではありません。 RFC 9110 によると、HEAD リクエスト内のコンテンツには「一般的に定義された意味論がない」ため、リクエストの意味を変更することはできず、「リクエスト・スマグリング攻撃の可能性を理由に、一部の実装ではリクエストを拒否し、接続を閉じる可能性がある」とされています。クライアントは HEAD リクエスト内でコンテンツを生成すべきではありません。
HEADリクエストは、プロキシが正常に動作しているかを確認するのに役立ちますか?
ある程度は有用です。接続性を確認でき、ステータスコードを低コストで取得できるため、簡易的な動作確認(スモークテスト)として適しています。ただし、実際の GET リクエストが成功するかどうかは判断できません。ボット対策レイヤーでは、これら 2 つのリクエストを別々に扱うことが多いためです。リスト全体で HEAD の結果を信頼する前に、サンプルに対して実際の GET リクエストで検証を行ってください。
まとめ
これについては、2つのコマンドで完全にカバーできます。適切なHEADリクエストを行うには curl -I URL を、実際のGETリクエストで生成されるヘッダーを取得したい場合は curl -sS -o /dev/null -D - URL を使用します。一方、-X HEAD は機能しません。マニュアルにも明記されている通り、これは単なる語句の変更に過ぎず、動作そのものは変わらないため、curlはサーバーが送信してはならないボディを待ち続けてしまうのです。
どちらを選ぶかは状況次第です。HEADは処理負荷が劇的に低く(ページ全体に対してわずか数百バイト程度)、リンクチェック、可用性監視、サイズ推定に最適なツールであり、特に帯域幅に制限がある環境では有効です。 しかし、実際の取得で何が返されるかを予測するツールとしては不適切です。なぜなら、サーバーはコンテンツに由来するヘッダーを省略することが許されており、HEADリクエストを完全に拒否したり、しばしば異なるロジックを通じて処理したりする可能性があるからです。
また、帯域幅を消費せずにGETと同等の精度が必要な場合は、-r 0-0があまり活用されていない中間的な手段となります。これは、1バイトのみを取得する本物のGETリクエストです。多くのサーバーは結局ファイル全体を返してしまうため、まずはAccept-Ranges: bytesを確認することをお勧めします。
