HTTP範囲リクエストの検証|Range・206・416でダウンロード再開を確かめる方法
PDF、動画、バックアップなど大きなファイルを配信するとき、途中まで取得したデータを続きから取り直せると通信量と待ち時間を抑えられます。そのために使われるのが、GETのRangeリクエストです。ただし、Rangeを送っただけで「必ず206が返る」「保存済みの末尾へ常に追加できる」と考えると、ファイルの途中に別の版が混ざる事故につながります。
範囲リクエストの検証では、開始位置と終了位置を0始まり・両端含みで計算し、206のContent-Rangeと本文バイト数、対象表現の版を照合します。サーバーはRangeを無視して200を返すことも仕様上許されるため、200を機械的な障害とせず、全体取得として先頭から扱えるかを確認します。範囲外では416とContent-Range: bytes */Nを確認し、再開条件が変わったときは保存済みデータへ継ぎ足しません。
単一範囲の取得から再開条件、範囲外、圧縮・CDN経由の順に確認すると、応答の違いを切り分けやすくなります。コマンドのURLは自社の検証用ファイルへ置き換えてください。

図に示したとおり、同じURLへ送った結果でも、206・200・416では次の操作が変わります。206は部分取得が成立した応答、200は全体取得として扱う応答、この単一範囲の検証で416は、要求した範囲を現在の表現から満たせない場合の応答です。どの結果でも、ステータスだけで結論を出さず、Content-Range、Content-Length、ETag、Content-Encodingを合わせて確認します。
本記事のポイント
- Rangeの位置は0始まり・終了位置を含むため、206の本文長は「終了−開始+1」で照合します。
- 単一範囲は応答ヘッダーのContent-Rangeを確認し、複数範囲は複数パートで返る場合に各パートを個別に照合します。
- If-Range不一致やRange無視の200は保存済み部分へ追加せず、表現の版と現在の長さを確認して受け直します。
1. まず対象の表現とバイト位置を固定する
HTTPのバイト範囲は、画面に表示されるページ番号や文字数ではなく、選択された表現のオクテット位置で指定します。bytes=0-99は先頭から100バイトで、開始位置も終了位置も含みます。bytes=100-199は次の100バイトです。したがって、単一範囲の期待本文長は次の式で求めます。
期待本文長 = last-position - first-position + 1
bytes=0-99 → 100バイト
bytes=100-199 → 100バイト
ファイルの全長をNバイトとすると、Content-Range: bytes 0-99/Nの「0-99」は今回届いた範囲、「N」は選択された表現全体の長さです。Content-Lengthが100なら本文の長さと一致しますが、これは全体の長さを示すとは限りません。206では、本文の長さとContent-Rangeの分母Nを別々に記録してください。
同じURLでも、認証、言語、クエリ、Accept-Encoding、時刻、CDNの状態によって選択される表現が変わる場合があります。取得済みの先頭部分と続きの部分を結合する前に、少なくとも強いETagなどの強い検証子、Content-Type、Content-Encoding、全長Nが同じかを確認します。強い検証子が一致しない部分は、バイト位置が同じでも同じファイルの続きとは扱いません。配信経路の全体像を先に整理する場合はウェブサイト構築の教科書も参照できます。
| 確認対象 | 見る値 | 合格の条件 |
|---|---|---|
| 位置 | RangeとContent-Rangeの開始・終了 | 要求と応答の範囲が想定どおりで、終了−開始+1が正しい |
| 本文長 | Content-Length(存在する場合)と実際の本文バイト数 | 単一範囲なら実バイト数が期待本文長と一致する |
| 表現の版 | 強いETag、必要ならLast-Modified | 保存済み部分と続きが同じ表現に属する |
| 表現形式 | Content-Type、Content-Encoding、Vary | 同じ形式・同じ符号化単位として比較できる |
次の例では、ヘッダーと本文を別ファイルへ保存します。example.comは例示用ドメインです。検証対象には300バイト以上の固定ファイルを使い、まず全体取得で長さと強いETagを記録すると、境界の比較が明確になります。
URL='https://example.com/files/sample.bin'
curl -sS -D /tmp/range-0-99.headers \
-H 'Range: bytes=0-99' \
-o /tmp/range-0-99.bin "$URL"
# 判定時に確認する値(実際のヘッダーを読んでから手動または検証スクリプトで照合)
wc -c < /tmp/range-0-99.bin
grep -i -E '^(HTTP|Content-Range:|Content-Length:|ETag:|Content-Encoding:)' /tmp/range-0-99.headers
保存後は、応答のステータスとヘッダー、実バイト数を確認します。サーバーがRangeに対応し、条件が成立すれば206が候補になりますが、サーバーはRangeを無視して200を返すこともあります。200になった場合は、その本文を保存済みの部分へ追加せず、全体を先頭から取得したものとして扱えるかを確認します。
2. 単一範囲と複数範囲を別の形式として照合する
単一範囲は、ダウンロード再開で最も扱いやすい形式です。たとえばRange: bytes=100-199に対して206が返るなら、応答ヘッダーのContent-Rangeはbytes 100-199/Nの形になり、本文は100バイトです。終了位置を含む点を忘れると、本文長を99バイトと数えたり、次のRangeを99から始めたりする境界ずれが起きます。
複数範囲は、Range: bytes=0-99,200-299のようにカンマで指定します。複数パートで返る206では、本文全体のContent-Typeがmultipart/byterangesになり、境界文字列で分けられた各パートに個別のContent-Rangeが入ります。複数パートのとき、HTTPヘッダー領域には単一範囲のような一つのContent-Rangeを置かず、各パートの値を読む必要があります。サーバーは範囲を結合して、単一の連続範囲の206を返すこともあります。単一範囲に対してサーバーがmultipartを生成することは、RFC 9110の規定に沿いません。
サーバーは、重なる範囲や間隔が小さい範囲をまとめることがあります。要求した順序と同じ順序で届くとも限らず、満たせない範囲が除外されることもあります。クライアントはパートの順番ではなく、各パートのContent-Rangeを見て配置します。multipartを処理できないクライアントは、複数範囲を要求しない設計にします。
| 要求 | 206で確認する形式 | 保存処理 |
|---|---|---|
bytes=100-199 | 応答ヘッダーにContent-Range: bytes 100-199/N、本文100バイト | 同じ版の100〜199へ配置する |
bytes=0-99,200-299 | 複数パートならmultipart/byteranges、各パートにContent-Range | 各パートの範囲を読んで個別に配置する |
| 重複・近接した複数範囲 | サーバーが結合した単一範囲の206になる場合がある | 要求文字列ではなく応答の範囲で配置する |
curl -sS -D /tmp/range-multi.headers \
-H 'Range: bytes=0-99,200-299' \
-o /tmp/range-multi.body "$URL"
grep -i -E '^(HTTP|Content-Type:|Content-Length:)' /tmp/range-multi.headers
# multipartの場合は、body内の各パートのContent-Rangeを読んでから保存先を決める
実際のバイト列を連結するのは、各部分の版、範囲、本文長が照合できた後です。単一範囲を複数回取得するテストでも、まず各部分を別ファイルへ保存し、検証に合格するまで保存済みファイルへ書き込みません。自動処理では、Content-Range、強いETag、Content-Encoding、実バイト数のすべてを確認できた場合だけ結合処理へ進む分岐を置きます。
3. If-Rangeで再開条件を確認し、200を継ぎ足さない
If-Rangeは、保存済みの表現とサーバー側の表現が同じときだけRangeを適用し、異なるときは全体を返してもらうための条件です。RFC 9110の13.1.5は、Rangeを含まないIf-Rangeは無視すること、Rangeをサポートしない対象ではIf-Rangeを無視すること、条件が偽ならRangeを無視することを定めています。
ETagを使う場合、If-Rangeへ弱いETagを入れてはいけません。ETag: W/"abc"のようにW/が付いた値は弱い検証子です。保存済みのレスポンスに弱いETagしかないなら、If-Rangeで部分結合を進めず、強い検証子を取得して全体を受け直すなど、同一表現を確認できる方法へ切り替えます。日付を使えるのは、対応する表現にETagがなく、しかもその日時が強い検証子として扱える場合です。
If-Rangeの条件が真で、指定範囲が満たせる場合は、サーバーはRangeを処理して206を返すことが推奨されています。条件が偽なら、サーバーはRangeを無視して全体の200を返します。この200は「部分取得の続き」ではありません。クライアントは保存済みの途中ファイルへ200本文を追加できず、保存済み部分を破棄して先頭から受け直すか、同一版の全体として置き換えます。
# 強いETagの値はテスト対象のレスポンスから取得したものに置き換える
curl -sS -D /tmp/range-resume.headers \
-H 'Range: bytes=100-199' \
-H 'If-Range: "strong-validator-from-response"' \
-o /tmp/range-resume.bin "$URL"
grep -i -E '^(HTTP|Content-Range:|Content-Length:|ETag:)' /tmp/range-resume.headers
# 206と期待範囲・同じ強い検証子を確認できなければ、既存ファイルへ追加しない
複数回の206を一つへ結合できるのも、同じ強い検証子を共有するときだけです。ETagはサーバーが選ぶ不透明な検証子であり、文字列をハッシュ値だと推測したり、URLが同じことだけで同一性を決めたりしません。CDNを挟む場合は、オリジンとエッジで検証子がどのように引き継がれるかも確認します。保存前の版を確認する方法はETag・Last-Modified・304の照合にも整理されています。
4. 416と200の扱いを範囲外テストで分ける
現在の表現がNバイトで、要求した範囲に一つも満たせる部分がない場合、または過剰な数の小さな範囲・重複範囲が拒否された場合、416(Range Not Satisfiable)が候補になります。単一範囲の範囲外テストでは、バイト範囲の416に対してサーバーがContent-Range: bytes */Nを生成することが推奨されています。ここでのNは、要求時点の選択された表現の現在の長さです。保存していたNと異なるなら、ファイルが更新された、表現の選択が変わった、または前回のメタデータが古い可能性を調べます。
次の開始位置999999999は例です。対象の現在長N以上になる値へ置き換えてください。1GBを超えるファイルでは、この値が範囲外にならないことがあります。また、416は過剰な小範囲や重複範囲を拒否するときにも使われるため、複数範囲のテストでは長さ以外の拒否理由も確認します。
curl -sS -D /tmp/range-out-of-bounds.headers \
-H 'Range: bytes=999999999-' \
-o /tmp/range-out-of-bounds.body "$URL"
grep -i -E '^(HTTP|Content-Range:|Content-Length:)' /tmp/range-out-of-bounds.headers
ただし、416を必ず返すことをサーバーの合格条件にしてはいけません。RFC 9110は、サーバーがRangeを無視して選択された表現全体を200で返すことを認めています。範囲外の要求でも200になる実装は、部分取得が成立しなかったという意味では注意が必要ですが、200を返したことだけで仕様違反とは断定できません。クライアント側では、200本文を保存済みの断片へ継ぎ足さず、全体取得として先頭から置き換えられるかを確認します。
長さ0のリソースも別扱いにします。RFC 9110は、選択された表現に内容がない場合、RangeをサポートするサーバーがRangeを無視してよいとしています。したがって、空ファイルへ範囲外の要求を送り、必ず416とbytes */0が返ることをテストの前提にしないでください。空の200や実装で定めた応答を、本文長0・対象リソースの存在・クライアントの再試行条件と一緒に評価します。
| 応答 | 意味 | 再開クライアントの動作 |
|---|---|---|
| 206 | 要求した部分の一部または全部が部分表現として届いた | 各Content-Range、本文長、強い検証子が一致した場合だけ配置・結合する |
| 200 | 全体表現が届いた。Rangeが無視された、またはIf-Range条件が偽の可能性がある | 既存の部分へ追加せず、先頭から全体を置き換える |
| 416 | 要求範囲を満たせない、または過剰な小範囲・重複範囲として拒否された | bytes */Nで現在長を確認し、再開位置と表現の版を見直す |
| 空の表現 | Rangeを無視してよい例外がある | 416を必須にせず、長さ0とリソース状態を別に確認する |
5. 圧縮表現とCDNを分けて検証する
Rangeのバイト位置は、クライアントが展開した後の文字列や動画フレームではなく、応答として選択された表現のバイト列に対して解釈します。Content-Encoding: gzipやbrが付いている応答では、Rangeの単位は符号化された表現のバイト列です。identityで取得したファイルの0〜99バイトと、gzip表現の0〜99バイトを同じ続きとして結合できません。圧縮交渉の詳細はContent-Encoding・Vary・二重圧縮の検証で確認できます。
圧縮を含むテストでは、Accept-Encoding: identityと圧縮を許すリクエストを別ケースにし、レスポンスのContent-Encoding、Vary、ETag、Content-Length、Content-Rangeを保存します。ブラウザやライブラリが自動展開する場合、保存したファイルがワイヤー上のバイト列なのか展開後のデータなのかも記録します。展開後のデータを位置指定の基準にしてしまうと、次のRangeの開始位置がずれます。
CDNやリバースプロキシがあるときは、オリジンへの応答と公開URLへの応答を同じケースで比べます。CDNがRangeをキャッシュキーや転送処理へどう反映するか、部分応答を再利用・結合するか、オリジンのETagやContent-Rangeをどう扱うかはサービスごとの仕様です。Accept-Ranges: bytesが返っても、すべての将来リクエストが206になる保証ではありません。別のエッジ、別の条件、内容更新によって応答が変わる可能性を前提にします。
| 経路 | 同じテストを取る場所 | 記録する差分 |
|---|---|---|
| オリジン | 管理されたテスト用ホスト | Range処理、強い検証子、圧縮、更新時刻 |
| CDN経由 | 利用者がアクセスする公開URL | Age、Via、キャッシュ状態、ETag、Content-Range、Vary |
| クライアント保存 | HTTPライブラリやブラウザの保存処理 | 自動展開の有無、本文バイト数、再開時の追加条件 |
CDNのキャッシュキー、Purge、範囲応答の扱いまで確認する場合は、経路ごとの設定と公開URLの応答を照合します。キャッシュルールを変更するときは、利用中のCDN公式仕様と、下の関連ページにある確認項目を突き合わせてください。
検証結果を記録する最小テンプレート
テスト結果はステータスだけでなく、要求・選択表現・応答・保存処理を一行で追える形にします。特に、200をエラーとして数えるか、416を必須にするかは、Rangeの仕様とクライアントの復旧方針を分けて記録します。
| 列 | 記録例 | 判断 |
|---|---|---|
| 要求 | GET /files/sample.bin、Range: bytes=100-199、If-Range: 強いETag | 単一範囲の再開ケース |
| 表現 | ETag、Content-Type、Content-Encoding、Vary、取得時刻 | 保存済み部分と同じ版・形式か |
| 応答 | 206 / 200 / 416、Content-Range、Content-Length、本文バイト数 | 部分配置、全体置換、範囲見直し |
| 処理 | 追加、先頭から置換、再試行、保留 | 実際にファイルへ反映した操作 |
| 経路 | オリジン、CDN、クライアント、キャッシュ状態 | 再現条件と責任範囲 |
本文を比較するときは、レスポンスヘッダーの記録とファイルのハッシュを同じ行へ残します。ETagはハッシュだと決めつけず、管理側が別途チェックサムを提供している場合だけ、その仕様に従って照合します。部分ファイルだけのハッシュは全体の同一性を保証しないため、比較できない場合は継ぎ足して見かけ上の完成ファイルを作らず、保留として原因を切り分けます。
よくある質問
Rangeの開始位置と終了位置はどう検証しますか?
bytesは0始まりで、終了位置を含みます。bytes=0-99なら本文は100バイト、bytes=100-199も100バイトです。応答のContent-Rangeが要求した範囲と一致し、本文の実バイト数が終了−開始+1になるかを確認します。
206の本文長とContent-Rangeはどう照合しますか?
単一範囲なら、たとえばContent-Range: bytes 100-199/Nに対し、実際の本文長が100バイトになるかを見ます。Content-Lengthがある場合はその値も照合し、省略だけを異常としません。Nは選択表現全体の長さです。multipartではHTTPヘッダーではなく各パートのContent-Rangeを読み、各パートの本文長を個別に照合します。
単一範囲と複数範囲の応答は何が違いますか?
単一範囲の206は応答ヘッダーにContent-Rangeがあり、本文が一つの範囲です。複数範囲が複数パートで返る206ではmultipart/byterangesとなり、各パートにContent-Rangeがあります。サーバーが範囲を結合して単一範囲にしたり、要求順と異なる順で返したりする可能性があるため、要求文字列ではなく実際の応答の範囲を見て配置します。
範囲外なら必ず416になりますか?
なりません。範囲を満たせない場合は416が候補で、Content-Range: bytes */Nが現在の長さを示します。一方、RFC 9110はサーバーがRangeを無視して全体を200で返すことも認めています。200を受けたら部分ファイルへ追加せず、全体取得として先頭から置き換えます。
If-Rangeに弱いETagを使えますか?
使えません。W/付きの弱いETagをIf-Rangeへ入れてはならず、弱い検証子しかない場合は部分結合を進めない判断が必要です。強いETagが一致すればRangeを処理する条件になり、不一致ならRangeを無視した全体200になるため、200本文を保存済みの途中へ継ぎ足しません。
圧縮やCDNがあるとき、どのバイトを比較しますか?
応答で選択された表現のバイト列を比較します。gzipやbrのContent-Encodingが付く場合は符号化されたバイト列であり、identityの範囲とは混ぜません。オリジンとCDNの両方でContent-Encoding、Vary、ETag、Content-Range、キャッシュ状態を記録し、同じ表現か確認します。
Range対応、再開処理、圧縮、CDNの条件が別々に管理されていて、どこから直すか決めにくい場合は、LPとWeb改善の設計を相談することができます。対象経路と利用者の保存処理を一緒に整理し、検証結果を次の改善へつなげます。
参考情報
- RFC 9110 Section 13.1.5 If-Range(Range適用条件、弱いETagの禁止、条件不一致時のRange無視)
- RFC 9110 Section 14.1.2 Byte Ranges(バイト範囲の構文と境界)
- RFC 9110 Section 14.2 Range(Rangeを無視できること、206・416の条件、空の表現)
- RFC 9110 Section 14.4 Content-Range(206と416の表示形式、
bytes */N) - RFC 9110 Section 15.3.7 206 Partial Content(単一範囲とmultipart/byteranges、結合条件)
- RFC 9110 Section 15.5.17 416 Range Not Satisfiable(416の意味とRange無視による200の注意)
- MDN Web Docs「Range header」(0始まり・両端含み、圧縮表現、実例の説明補助)
- MDN Web Docs「If-Range header」(再開条件の説明補助)