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
| Cause | Clue | Fix |
|---|---|---|
| No range support in the app or server | No Accept-Ranges header; the response is generated or streamed by code | Serve 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 compression | Content-Encoding: gzip or br on the reply; adding -H "Accept-Encoding: identity" makes it work | Don’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 match | The client sent If-Range with an old ETag or date | Working 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 layer | Works against the origin directly, fails through the public URL | Check the proxy’s range or buffering settings; for nginx as a reverse proxy, the upstream must support ranges itself |
| Malformed or unsupported range | Typos such as bytes 0-99 (space instead of =), units other than bytes, or many overlapping ranges | Use exactly Range: bytes=start-end. Servers may legally ignore odd or excessive ranges |
| Wrong method | You sent Range on a POST or PUT | Range is only defined for GET (and is ignored on HEAD) |
Why it matters
- Video and audio seeking. Browsers jump through a
<video>by requesting ranges. A server that always sends 200 makes the seek bar useless, and Safari is particularly strict about it. - Resuming downloads. curl stops with error 33 and wget refuses to continue: see resume a download with curl or wget.
- Bandwidth. Each seek or retry pulls the whole file again.
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 .