Range request returns 200 instead of 206: why the server ignored it

You asked for bytes 0–99 and the server sent all 700 MB with a cheerful 200 OK. That is allowed, but it is usually a sign that one link in the chain does not do ranges.

The test

curl -s -o /dev/null -D - -H "Range: bytes=0-99" https://example.com/file.bin

A working server answers 206 with Content-Range: bytes 0-99/<size> and Content-Length: 100. If you see 200 and the full length, work down this list.

Causes, and how to tell them apart

CauseClueFix
No range support in the app or serverNo Accept-Ranges header; the response is generated or streamed by codeServe the file with the web server’s static handler or a helper that supports ranges (res.sendFile, nginx X-Accel-Redirect, Apache X-Sendfile)
On-the-fly compressionContent-Encoding: gzip or br on the reply; adding -H "Accept-Encoding: identity" makes it workDon’t compress already-compressed files (video, ZIP, images); exclude large downloads from gzip; or pre-compress the file once so its bytes are stable
If-Range did not matchThe client sent If-Range with an old ETag or dateWorking as designed: the file changed, so the client must start over. Make sure the ETag is stable across servers (no per-machine inode in it)
Proxy, CDN or security layerWorks against the origin directly, fails through the public URLCheck the proxy’s range or buffering settings; for nginx as a reverse proxy, the upstream must support ranges itself
Malformed or unsupported rangeTypos such as bytes 0-99 (space instead of =), units other than bytes, or many overlapping rangesUse exactly Range: bytes=start-end. Servers may legally ignore odd or excessive ranges
Wrong methodYou sent Range on a POST or PUTRange is only defined for GET (and is ignored on HEAD)

Why it matters

Checking an Express or Node handler

// Supports Range, ETag and Last-Modified out of the box
app.get("/files/:name", (req, res) => res.sendFile(safePath(req.params.name)));

// Does NOT support Range: always a 200 with the whole file
app.get("/raw/:name", (req, res) => fs.createReadStream(safePath(req.params.name)).pipe(res));

safePath stands for your own check that keeps the name inside the download folder.

Related

HTTP 206 Partial Content · Accept-Ranges header · Content-Range header · Download won't resume · HTTP 304 Not Modified · 416 Range Not Satisfiable

Questions

Why does my server return 200 instead of 206?

It ignored the Range header, which HTTP allows. Usually the response has no range support, is compressed on the fly, failed an If-Range check, or a proxy stripped the header.

Does gzip break range requests?

Compressing on the fly usually disables them, because the compressed bytes are not stable. Exclude large or already-compressed files from compression.

Why does If-Range give me the whole file?

If-Range says: send the range only if the file is unchanged. If the ETag or date no longer matches, the server sends the whole new file with 200 on purpose.

Why can't I seek in my video?

Browsers seek by requesting byte ranges. If the server always answers 200, seeking fails or restarts the video. Serve the file with range support.

Is it a bug for a server to ignore Range?

No, it is allowed, but clients then cannot resume or seek. For downloads and media you almost always want range support.

Published .

Try it

Related guides

Newest guides