Sign in
Log inSign up
Download Monitoring: A Cross-Browser Story

Download Monitoring: A Cross-Browser Story

G.'s photo
G.
·Jan 12, 2019

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 by onprogress when it reached the 100% mark
  • until version 34, when a Content-Encoding response header field was present the loaded property reflected the number of bytes after decompression instead of the raw bytes transferred which resulted—if a Content-Length was sent by the server—in loaded exceeding total 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.

  • if lengthComputable === false—i.e. the Content-Length response header is missing—total and totalSize used to return UINT64_MAX instead of 0
  • when the Content-Encoding is set, total erroneously returns 0 even if the Content-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

student

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?