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, the
addEventListenervariant ofonprogresswasn't supported - between version 3.5 and 8, you had to fallback on the
onloadhandler to compensate for the inane absence of the last progress event that used to be fired byonprogresswhen it reached the 100% mark - until version 34, when a
Content-Encodingresponse header field was present theloadedproperty reflected the number of bytes after decompression instead of the raw bytes transferred which resulted—if aContent-Lengthwas sent by the server—inloadedexceedingtotalonce 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-Lengthresponse header is missing—totalandtotalSizeused to return UINT64_MAX instead of0 - when the
Content-Encodingis set,totalerroneously returns0even if theContent-Lengthis 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
responseTypeto 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? Does it belong on dev.to?
Top comments (0)