DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

HTTP Range Has Three Outcomes, Not Two: 32.03% Ignored, 54.95% Partial, 13.02% 416 Over 10,116 Calls

The Range header is behind every resumed download, every seek in a <video> tag and every byte-serving PDF viewer. It has three outcomes, and servers routinely collapse them to two.

Over an enumerated corpus of 1,124 headers × 9 representation lengths = 10,116 decisions: 3,240 ignored → 200 (32.03%), 5,559 → 206 (54.95%), 1,317 → 416 (13.02%). A malformed field is 2.46× more common than an unsatisfiable one — and those are exactly the pair you conflate by treating "invalid" as "unsatisfiable".

👉 Live, runs in your browser: https://dev48v.infy.uk/solve/day80-http-range-parser.html

Invalid is not unsatisfiable

the field does not parse   -> 200, whole body      (it is ignored, not rejected)
it parses, nothing fits    -> 416 + bytes */len    (nothing else ever gives 416)
it parses, something fits  -> 206, the rest dropped
Enter fullscreen mode Exit fullscreen mode

RFC 7233 is explicit that a Range a recipient cannot parse is dropped — which means an ordinary 200 with the whole body. 416 exists for exactly one situation: the field parsed cleanly and every member starts past the end.

So bytes=9999- against a 1,000-byte file is a 416, and bytes=abc is a 200. Getting those backwards is how a byte-serving endpoint answers 416 to clients that should have received the whole file.

A single backwards member poisons the whole header

This is the one almost everybody gets wrong. bytes=0-99,7-3 is not "one good range and one bad one":

// the rule everyone gets wrong: this poisons the FIELD, not the member
if (last !== null && first > last)
  return bad("first-byte-pos " + first + " is greater than last-byte-pos " + last);
Enter fullscreen mode Exit fullscreen mode

first > last makes the field syntactically invalid, so the whole thing is ignored and the server sends 200 with the entire body. The most obviously "bad" range in the list lands in the 200 column, not the 416 one.

Only first-byte-pos can push you off the end

// only first-byte-pos can make a range unsatisfiable - last merely clamps
if (spec.first > len - 1) return null;
const last = spec.last === null ? len - 1 : Math.min(spec.last, len - 1);
Enter fullscreen mode Exit fullscreen mode

A suffix longer than the representation clamps to the whole representation. A last past the end clamps to len-1. Which is why, against the same 1,000-byte file, bytes=900-9999 is a 206 and bytes=9999- is a 416.

Three spellings of "give me everything" — bytes=0-, bytes=-len and bytes=0-(len-1) — resolve to one identical range at every non-zero length. At length 0 the identity is scoped out rather than fudged: there is no whole representation to name, and 0-(len-1) spells 0--1, which is not a legal spec at all.

The middle case is the majority, and it shrinks twice

When some members fit and some do not, the unsatisfiable ones are dropped and what remains is served as a 206. Not a 416, not an error. 2,152 decisions land in that case.

Coalescing then shrinks the answer again: 5,044 of the 5,559 206s emit fewer parts than the client wrote. Adjacency is the subtle half —

// adjacent, not merely overlapping: 0-99 and 100-199 are ONE part
if (prev && s[i].first <= prev.last + 1) prev.last = Math.max(prev.last, s[i].last);
Enter fullscreen mode Exit fullscreen mode

0-99 and 100-199 share no byte, but they are one contiguous run, and a server that ships them as two multipart parts is paying boundary overhead for nothing.

Six invariants over every one of the 10,116 decisions, 0 violations — including the one that actually bites: the emitted byte set is rebuilt as a plain set union of the satisfiable members and compared in both directions, rather than against itself. And 416 is asserted as an if-and-only-if, so neither direction can rot alone.

What this covers, and what it does not

It is a parser, not a complete server. Out of scope, each changing real behaviour: If-Range and the conditional dance that decides whether a range is honoured at all, Accept-Ranges, the actual multipart body bytes with their boundaries and CRLFs, Content-Encoding interacting with byte offsets, and ranges against a representation whose length is not yet known.

Three strictness choices are documented rather than hidden, because a different implementer could defensibly go the other way: an empty list element such as bytes=0-1,,4-5 is treated as invalid where RFC 7230's list rule would let a recipient skip it; whitespace around the = is rejected because the grammar has none there; and bytes=-0 is treated as unsatisfiable, since a zero-length suffix selects no bytes at all.

37 in-page checks, 202 verifier asserts, 0 failures. Vanilla JS, one file, no build step.

Top comments (0)