Claude が書いた記事
Content-Range と Content-Length ── サイズのズレが Front Door で 503 を招くとき
Content-Range が `bytes 0-848/849` やのにボディが 879 バイトあったらどうなる? その矛盾から、Content-Range と Content-Length が「誰のサイズ」かを整理して、ズレが本番 Azure Front Door で 503 を招きうる筋まで辿った学習ログ。
俺の最初の疑問
きっかけは Content-Range ヘッダを調べてたこと。大きいファイルの一部分だけをダウンロードするとき、サーバーが「全体のここからここまでを返したで」と伝えるレスポンスヘッダ、というのは分かった。でも、ひとつ具体的なところで詰まった。
Content-Range: bytes 0-848/849って宣言してるのに、実際のボディが 879 バイトあったらどうなるん?
さらにもう一つ。俺が実務で触ってる Azure Front Door は、こういう矛盾したレスポンスをどう扱うんや? この 2 つを潰した記録。
まず一言でいうと
Content-Range と Content-Length は、どっちもバイト数を言うてるのに「数えてる対象」が違う。
- Content-Range … 圧縮を解いた後の生データ (RFC 9110 では選択表現 = selected representation と呼ぶ) の、どの範囲かと全体サイズ。
- Content-Length … いま回線を実際に流れてるメッセージボディのバイト数。
圧縮が無ければ、この 2 つは一致する。だから bytes 0-848/849 でボディが 879 バイトやったら、圧縮なしなら仕様違反 (サーバーのバグ)。そして、このズレは机上の話で終わらへん。前段に CDN を置くと、サイズの食い違いを含むレスポンスで Front Door が 503 を返す可能性がある。ここまで地続きや。
何と比べるとわかるか
まず混同しやすい Range と Content-Range は、向きが逆や。
| ヘッダ | 向き | 意味 |
|---|---|---|
Range | クライアント → サーバー | 「全体のここからここまでくれ」(リクエスト) |
Content-Range | サーバー → クライアント | 「全体のここからここまでを返した」(レスポンス) |
クライアントが Range で一部を要求し、サーバーが応えるときのステータスは 200 やなくて 206 Partial Content。このとき Content-Length は全体のサイズやなくて、今回返す部分のサイズになる。ここが一つ目の罠。
何が問題なのか ── 3 つの値が指すもの
最初の疑問に戻る。Content-Range: bytes 0-848/849 でボディが 879 バイト。ここには別々のものを指す 3 つの数が出てくる。
- Content-Range の範囲 …
0-848= 計 849 バイト (終了 − 開始 + 1)。全体も 849。 - Content-Length … 本来ここに来るべき値。
- 実際に届いたボディ … 879 バイト。
圧縮が無ければ、この 3 つは全部一致しなあかん。1 つでもズレたら、受け取り側はどれを信じるかで挙動が割れる。
Content-Length: 849を信じて 849 バイトだけ読む → 残り 30 バイトをゴミ扱い → Keep-Alive で次のレスポンスの先頭と誤認して、次の通信が壊れる。- サイズ不一致を「転送が途中で切れた」と判断してダウンロード失敗。
- 並列ダウンローダが範囲を無視して 879 バイトを書き込む → 後続チャンクとのオフセットがズレて最終ファイルが破損。
要は、宣言したサイズと実ボディが食い違うと、どこかで必ず壊れる。
gzip が入ると話が変わる ── レイヤーが割れる
ここが一番の山や。「圧縮されてるからボディが膨らんだのでは?」と思うかもしれん。実は、圧縮があるとサイズが食い違っても合法になる。理由は、2 つのヘッダが見てるレイヤーが違うから。
Content-Rangeが数えるのは 圧縮を解いた生データ (選択表現) の範囲。Content-Lengthが数えるのは 圧縮した後に回線を流れる実ボディ。
だから、生データ 849 バイトを gzip で 300 バイトに縮めて返すなら、こうなる ── そしてこれは RFC 9110 (HTTP Semantics) のとおりや。Content-Range は選択表現 (圧縮前) の範囲を、Content-Length はメッセージボディ (圧縮後) を指す、と定義が分かれてるから合法。
HTTP/1.1 206 Partial Content
Content-Encoding: gzip
Content-Range: bytes 0-848/849 ← 圧縮前 (生データ) の範囲を数える
Content-Length: 300 ← 圧縮後の、いま流れてる実ボディを数える
849 ≠ 300 やのに違反やない。別々のものを数えてるから。逆に、Content-Encoding が無いのにサイズが食い違ってたら、それは紛れもないバグ。判定はシンプルや。
| 状況 | 判定 |
|---|---|
| 圧縮なし・範囲サイズ ≠ Content-Length | 仕様違反 (サーバーのバグ) |
| 圧縮あり・範囲サイズ ≠ Content-Length | 正常 (見てる層が違うだけ) |
図で見る
圧縮の有無で、Content-Range と Content-Length がどこを測ってるかを並べるとこうなる。
混乱しやすいポイント
① 「ボディのサイズ」は常に Content-Length が正
圧縮してようがしてまいが、テキストやろうが画像やろうが、回線を実際に流れるボディのバイト数 = Content-Length は 1 バイトもズレたらあかん。ズレると「途中で切れた」か「いつまでも終わらん」のどっちかになる。Content-Range の方は「圧縮を解いたら何バイトになるか」を指してるだけで、回線上のサイズやない。
② Content-Range の * は「不明」
Content-Range: bytes 200-1000/* の * は「全体サイズは今は不明」。逆に範囲を満たせないとき (要求がファイルサイズ超え等) は 416 Range Not Satisfiable + Content-Range: bytes */5000 で「範囲は返せん、なお全体は 5000」と伝える。
③ 違反かどうかは Content-Encoding を見てから言う
「Content-Range と Content-Length が違う = 違反」と短絡したらあかん。先に Content-Encoding があるか見る。あれば層が違うだけで正常、無ければバグ。この一手間を飛ばすと誤判定する。
たとえ話
伝票と、実際の小包。Content-Range は「中身を広げたら全部で 849 グラム、そのうち頭から 848 グラム目まで入れた」と書いた伝票。Content-Length は「いま運送屋が運んでる小包の実重量」。真空パックで圧縮して送れば、伝票は 849 グラム (開けたときの重さ) でも、小包の実重量は 300 グラムでええ ── 測ってる対象が違うから矛盾やない。でも真空パックもしてへんのに伝票と実重量が食い違うなら、それは詰め間違いや。
本番ではこう出る ── Azure Front Door の 503
ここからが二つ目の疑問。このサイズ矛盾、CDN を前段に置くと現実の障害になりうる。Azure Front Door (以下 AFD) は、オリジンから一貫しないサイズのレスポンスを受けると、安全側に倒してクライアントへ 503 を返すことがある。どういうときにそうなるかは、AFD の中の オブジェクトチャンキング という仕組みを知ると見えてくる。
レンジリクエストとオブジェクトチャンキングは別もの
混ざりやすいけど、主体が違う。そして大事なのは、別概念やのに片方がもう片方の上に乗ってること ── チャンキングは「AFD 自身がオリジンにレンジリクエストを投げる」ことで実装されてる。
| レンジリクエスト | オブジェクトチャンキング | |
|---|---|---|
| 主体 | クライアント (AFD も内部でこの役) | AFD (内部) |
| 正体 | HTTP 標準の機能 | AFD の配信最適化の仕組み |
| やること | 「一部だけくれ」と要求する | 大ファイルを 8 MB 単位で裏取り&キャッシュ |
| 関係 | 手段 | この手段を使って組み立てる |
AFD はキャッシュ有効時、大きなファイルをオリジンから 8 MB のチャンク単位で取りに行く。このとき、クライアントが全体を要求してても、AFD 自身がファイルを 8 MB ずつに区切ってレンジリクエストをオリジンへ発行する (Range: bytes=0-8388607、次は 8388608-16777215…) ── ここで AFD は「オリジンに対するクライアント」として振る舞う (だからオリジンがレンジリクエストに対応してる必要がある)。届いたチャンクを即キャッシュしてユーザーに返しつつ、次のチャンクを並列で先読みする。ファイル全体が揃ってなくても、手元のチャンク単位でキャッシュとして機能する。
ここで大事なのは、この 8 MB チャンクはあくまで AFD とオリジンの間の話で、クライアントには見えへんこと。クライアントから見えるのは 1 本のレスポンス ── 全体を要求したら 200 で全体のボディ、Range を出してたら 206 でその範囲。AFD は内部チャンクを繋いで 1 本のボディに仕立てて返す (しかも貯めてから送るんやなく、届いた順に流す)。だから AFD は最終的に 1 本の整合したレスポンスを組まなあかん。次の節の 503 は、ここで整合が組めなくなる話や。
なぜサイズのズレで 503 が起きうるか
AFD がチャンクを繋ぎ合わせて配信してる最中に、オリジンが返す Content-Length が一貫しなかったら、パズルが組めなくなる。Microsoft Learn が具体的に挙げてるのは Range と Accept-Encoding (圧縮要求) が同時に来たときや。同じ GET に対してオリジンが「ある時は圧縮後のサイズ、ある時は圧縮前のサイズ」と食い違う Content-Length を返すと、AFD は整合を取れず、データ破損やキャッシュ汚染を避けるために、503 を返すことがある。
(注意: ドキュメントが明記してるのはこの「範囲リクエスト + 圧縮で Content-Length がブレる」ケース。「Content-Range と Content-Length の数字が一致しなければ必ず 503」とまでは書かれてへん。だからここは「503 になりうる」と読むのが正しい。)
どう直すか ── まずオリジンを直す
本筋はオリジンを直すこと。それができれば回避策は要らん。回避策は「オリジンをすぐ直せないとき」の逃げや。
- 根本対処:オリジンを直す。 原因は「オリジンが同じ範囲リクエストに一貫しない
Content-Lengthを返す」こと。Range と圧縮の処理を直して、実ボディと一致するContent-Lengthを毎回返すようにすれば、原因そのものが消える。これで終わり。 - 回避策:オリジンをすぐ直せないとき。 原因は残したまま、矛盾の発生だけ止める当座しのぎ。
- オリジンか AFD の圧縮を切る (Range と圧縮の競合をなくす)。
- AFD のルールエンジンで範囲リクエストから
Accept-Encodingを剥がす (オリジンに圧縮させない)。
回避策は症状を止めるだけで、オリジンのバグは残る。だから回避策で済ませず、直せるならオリジンを直す。
出てきたヘッダの早見表
このページで出てきたヘッダを、向き (リクエストかレスポンスか) で分けてまとめる。Range と Content-Range、Accept-Encoding と Content-Encoding は名前が似てるけど向きが逆や。
| ヘッダ | 向き | このページでの役割 |
|---|---|---|
Range | リクエスト (クライアント → サーバー) | 「全体のここからここまでくれ」と部分を要求する |
Accept-Encoding | リクエスト (クライアント → サーバー) | 「gzip 等の圧縮を受け取れる」と申告する。Range と同時に来ると 503 の火種になりうる |
Content-Range | レスポンス (サーバー → クライアント) | 「全体の何バイト目〜何バイト目を返したか」。数えるのは選択表現 (圧縮前) |
Content-Length | レスポンス (サーバー → クライアント) | いま回線を流れる実ボディのバイト数 (圧縮後)。※リクエストにボディがある時はリクエストにも付く |
Content-Encoding | レスポンス (サーバー → クライアント) | ボディにかけた圧縮 (gzip 等)。これの有無で「サイズ違い = 違反か」が決まる |
(206 Partial Content・416 Range Not Satisfiable・503 Service Unavailable はヘッダやなくてステータスコード。レスポンスの 1 行目に乗る。)
で、この 503 が出たとき、圧縮をどこにやらせるかを選べるか?
- オリジンで圧縮するか、エッジ (Front Door) に一任するか。 判断軸は「Range と圧縮を両立できるか」と「8 MB の境界」── AFD がエッジで即時圧縮する対象は 8 MB 未満のファイルだけ (公式の「圧縮のルール」: MIME 種・1 KB 超・8 MB 未満)。オリジンが Range を返しつつ圧縮もかけて
Content-Lengthがブレるなら、オリジンでの圧縮をやめて圧縮の担当を Front Door (エッジ) 側に移すか、ルールエンジンで範囲リクエストからAccept-Encodingを剥がす。 - 責任境界はどこか。
Content-Lengthと実ボディの整合を保証するのはオリジンの責任。それをチャンクに割って束ねるのが AFD の責任。503 を見たら「整合が崩れてるのはオリジン側」とまず切り分けられる。
これに答えられると強い ── 障害を「CDN のせい」で止めず、圧縮とキャッシュの責任境界を引いて、どっちを直すか即決できる側に立てる。
この記事の続きとして、次に掘るといいトピック。
- キャッシュは実際どう保管されるか ── 8 MB チャンクはディスクにどう置かれ、ハッシュ名と索引でどう引かれるか。この記事の「チャンク単位キャッシュ」の中身に一段降りる話 (同じ持ち込みの続き)。
Transfer-Encoding: chunked── 長さを前置きせず「チャンクごとに長さを書く」もう一つの送り方。Content-Lengthを出せないストリーミングとの対比。- AFD のルールエンジン ── ヘッダを条件で書き換える仕組み。503 回避で
Accept-Encodingを剥がす実装の出口。 - ETag と条件付きリクエスト ── 中断したダウンロードを途中から再開するとき、続きのファイルが「同じ版か」をどう保証するか。
Rangeの隣のピース。