# Range Requests HTTP `Range` requests (`Range: bytes=N-M`) allow clients to fetch a portion of a response body. Relaycache handles these with a "full-fetch upgrade" strategy. ## Strategy: always cache the full body Relaycache never caches partial responses. When a range is requested, Relaycache fetches (or uses) the **full body** and slices it for the client. This means: - The **first** range request for an uncached resource causes a full download - **All subsequent** requests (ranged or not) are served from the full cached body This trades first-request latency for dramatically better subsequent performance. It is the same strategy used by production container registries (Harbor, Nexus). ## Case 1: Full body already cached The most important case. Client sends `Range: bytes=N-M`, proxy has the full body cached. ``` Client: GET /blob Range: bytes=0-999 If-Range: "etag-client-has" (optional) Relaycache strips Range and If-Range before forwarding: Relaycache→Origin: GET /blob If-None-Match: "cached-etag" Origin → 304 Not Modified Relaycache serves bytes [0, 1000) from cache as 206 No body re-download. Auth was checked. ✅ Origin → 200 (ETag changed) Relaycache downloads new body, updates cache Serves bytes [0, 1000) from new body as 206 ✅ ``` Why strip `Range`? Because we want a full-file conditional GET (`If-None-Match`) so the origin can respond `304`. If we forwarded `Range`, the origin would respond `206` (not `304`), losing the ability to reuse the cached body. ## Case 2: No cache, client sends Range ``` Client: GET /blob Range: bytes=1000-1999 Relaycache has no cache entry. Upgrade to full fetch: Relaycache→Origin: GET /blob (no Range header) Origin → 200 with full body Relaycache caches full body Serves bytes [1000, 2000) as 206 to client ✅ Origin → body > --max-cacheable-size Relaycache forwards the upstream 206 as-is (pass-through) Nothing cached ✅ ``` ## Case 3: Range beyond end of file ``` Client: GET /blob Range: bytes=9999-99999 (file is 1000 bytes) Relaycache: range is unsatisfiable → 416 Range Not Satisfiable Content-Range: bytes */1000 ``` ## Case 4: Multi-range (`bytes=0-9,20-29`) Not supported. Relaycache falls through to a plain `200` response with the full body. Multi-range responses require `multipart/byteranges` body assembly which adds significant complexity for a rare use case. ## The `If-Range` header `If-Range` is sent by clients that have a partial response and want to resume or extend it: "give me this range, but only if the ETag still matches; if not, give me the whole new file." Relaycache strips `If-Range` when it has the full body cached (Case 1) because Relaycache handles the conditional check itself via `If-None-Match`. The client's `If-Range` is irrelevant — Relaycache has the full body regardless. When there is no cache (Case 2), `If-Range` is also stripped because Relaycache upgrades to a full unconditional fetch. ## 206 response construction When serving a range from cache, Relaycache constructs the `206` response by: 1. Copying the stored response headers (Content-Type, ETag, Last-Modified, etc.) 2. Setting `Content-Range: bytes start-end/total` (inclusive end per RFC) 3. Setting `Content-Length` to the slice length 4. Setting `Accept-Ranges: bytes` 5. Overlaying any end-to-end headers from the origin's `304` (updated ETag, etc.) 6. Setting `X-Cache: HIT` ## Auth enforcement on range requests Even when serving a range from cache, Relaycache **always contacts the origin first**. The sequence for a cached range hit: ``` 1. Client sends Range request 2. Relaycache forwards to origin with If-None-Match (no Range) 3. Origin checks auth → if 401/403, return that to client; do not serve range 4. Origin checks freshness → 304 5. Relaycache slices cached body → 206 to client ``` A user whose access is revoked gets `401` at step 3, never the cached bytes.