Preamble
When you want to implement something in a cross-browser way, you are in for a ride down the bugtracker hole. After some exhaustingthorough research, I felt the urge to share my findings on XMLHttpRequest.prototype.onprogress
.
Rationale—why fetch doesn't cut it
Before going further, I'd like to explain why I prefer XMLHttpRequest
over fetch
for download monitoring: browser vendors didn't ship Response.prototype.body
from the get go i.e. fetch
didn't support it initially.
interface ProgressEvent : Event {
readonly attribute boolean lengthComputable;
readonly attribute unsigned long long loaded;
readonly attribute unsigned long long total;
};
And even if the browsers that you currently target do provide that readable stream, XMLHttpRequest
would remain the superior choice for an arcane discrepancy: when the content-length
response header is present but not exposed, total
will be populated with the response body's size irregardless of the Access-Control-Expose-Headers
field's value.
Genesis
interface LSProgressEvent : Event {
readonly attribute unsigned long position;
readonly attribute unsigned long totalSize;
};
Its first incarnation was implemented by Firefox 0.9.3! Back then the ProgressEvent
interface didn't exist so they relied on the little known LSProgressEvent
interface; to remain compatible WebKit had to support both interfaces until Mozilla finally dropped the latter.
interface XMLHttpRequest : XMLHttpRequestEventTarget {
…
attribute EventHandler onprogress;
attribute EventHandler onreadystatechange;
…
};
For other browsers you had to fallback on XMLHttpRequest.prototype.onreadystatechange
which had its own shortcomings. Sadly, the native version of XMLHttpRequest
introduced in Internet Explorer 7 didn't expose partial results.
Browsers' Defects
Mozilla
Probably due to their early implementation, Gecko-powered browsers had many bugs to account for, notably:
- until version 9, DOM2 event registration—
addEventListener("progress", function (e) { ... })
—wasn't supported - between version 3.5 and 8, you had to fallback on the
onload
handler to compensate for the inane absence of the last progress event that used to be fired byonprogress
when it reached the 100% mark - until version 34, when a
Content-Encoding
response header field was present theloaded
property reflected the number of bytes after decompression instead of the raw bytes transferred which resulted—if aContent-Length
was sent by the server—inloaded
exceedingtotal
once all the data was received
Microsoft
Internet Explorer 8 brought the non-standard XDomainRequest.prototype.onprogress
. Since it didn't pass any arguments to the callback you had to track XDomainRequest.prototype.responseText
from within the closure. We had to wait another 3 years for Internet Explorer 10 to finally support all XMLHttpRequest Level 2 events—progress included.
WebKit/Blink
- if
lengthComputable === false
—i.e. theContent-Length
response header is missing—total
andtotalSize
used to return UINT64_MAX instead of0
- when the
Content-Encoding
is set,total
erroneously returns0
even if theContent-Length
is positive
Opera 12
interface XMLHttpRequest : XMLHttpRequestEventTarget {
…
void overrideMimeType(DOMString mime);
attribute XMLHttpRequestResponseType responseType;
…
};
For the loaded
property to be accurate relative to the total
property, the response body had to be treated as binary. To that end you had 2 possibilities:
- setting the
responseType
to either"blob"
or"arraybuffer"
- tampering with the media type using
overrideMimeType
Why?!
If you are wondering why I know so much about these quirks, it comes down to me being the maintainer of cb-fetch, a cross-browser HTTP client that abstracts away all this mess for you. Well it does way more than that, by all means check it out!
My goal is to reach 100 stars on GitHub before the next release.
Archaeology
I consider myself an API archaeologist. Do you like that kind of exhaustive examination of a subject? Is this the kind of post that you expect to find in your hashnode feed?