1991-2016—25 years of Art & Logic

jQuery Ajax Blobs and Array Buffers

Alien_blob_monster

A big part of what makes jQuery a regular part of so many web projects is the clean interface it offers us for a number of sometimes messy built-in aspects of javascript. The most obvious is the DOM interface; and in second place, jquery ajax and its various shorthand methods. Abstracting away the difference between ActiveXObject and XMLHttpRequest is one of the most obvious benefits – but even if you don’t need to worry about supporting old versions of IE, you might well enjoy the clean, object-based, promise-returning interface that jquery ajax offers.

It’s a shame then, that if you want to take advantage of XMLHttpRequest Level 2 features like Blob and ArrayBuffer uploading/downloading, you have to fall back to the standard javascript api.

Let’s fix that, shall we?

First, let’s have an example of what recieving a Blob might look like with the standard api:

var xhr = new XMLHttpRequest();

xhr.addEventListener('load', function(){
	if (xhr.status == 200){
		//Do something with xhr.response (not responseText), which should be a Blob
	}
});

xhr.open('GET', 'http://target.url');
xhr.responseType = 'blob';
xhr.send(null);

It’s not terrible, but we’re sure to miss being able to dangle our .done and .fail blocks off of our nice, compact $.ajax request. What we want is to be able to do something like this:

$.ajax({
	dataType:'blob',
	type:'GET',
	url:'http://target.url'
}).done(function(blob){
	// Do something with the Blob returned to us from the ajax request
});

So, we’re going to borrow a trick we discussed back in our Ajax Caching article, and create an Ajax Transport to handle sending and receiving Blobs and ArrayBuffers.

Final code is included at the end of the article (for those who want to get straight to the good stuff), but let’s walk through it and discuss what’s happening.

$.ajaxTransport("+*", function(options, originalOptions, jqXHR){

Notice the “+*” string? If you recall from our Ajax Caching article, this string indicates what dataType we want to match against when considering whether to use the transports defined in the following function. There we set type ‘json’, since we only wanted to cache json replies. Here though, we set ‘*’ to match all types (we’ll narrow our definition down inside the function proper – we just want to make sure that our function gets a crack at providing the transport whenever appropriate, and the expected return dataType could be anything if we’re sending a blob or arraybuffer).

But what’s with the ‘+’ preceeding the ‘*’? That’s a little (undocumented) trick found from perusing the jquery source – it indicates we want our transport to be prepended to the list of available transports, as opposed to appended. When matching against all types, we need to include this to prevent the standard transport factory from hogging the request.

if (window.FormData && ((options.dataType && (options.dataType == 'blob' || options.dataType == 'arraybuffer'))
        || (options.data && ((window.Blob && options.data instanceof Blob)
            || (window.ArrayBuffer && options.data instanceof ArrayBuffer)))
        ))
    {

I told you we’d narrow down our definition. 🙂
So what’s this monster if statement checking against? First, we check against window.FormData; as a feature of XMLHttpRequest Level 2, its a reasonable way to feature detect whether the browser is ready to provide us the necessary features for blob/arraybuffer sending/receiving.

Next, we check to see whether a dataType has been defined, and if so, whether the dataType is blob or arraybuffer, indicating we’re expecting our request to return us one of these data types.

Otherwise, we check to see whether data has been defined, and if so, whether the browser supports Blobs and our data is an instance of Blob, or the same for array buffers. If the browser supports XMLHttpRequest 2 and we’re either sending or receiving a blob/arraybuffer (or sending AND receiving), then we provide the transport. Remember, for $.ajaxTransport, we indicate we’re providing the transport by returning something. Returning nothing indicates that the next transport factory should try to handle this request.

		return {
            send: function(headers, completeCallback){
                var xhr = new XMLHttpRequest(),
                    url = options.url || window.location.href,
                    type = options.type || 'GET',
                    dataType = options.dataType || 'text',
                    data = options.data || null,
                    async = options.async || true;

                xhr.addEventListener('load', function(){
                    var res = {};

                    res[dataType] = xhr.response;
                    completeCallback(xhr.status, xhr.statusText, res, xhr.getAllResponseHeaders());
                });

                xhr.open(type, url, async);
                xhr.responseType = dataType;
                xhr.send(data);
            },
            abort: function(){
                jqXHR.abort();
            }
        };

As you’ll recall from our Ajax Caching article, we need to provide two functions for a transport – send, and abort. The body of send should look familiar here – we consider the options set in the ajax options block, and set reasonable defaults if the needed options aren’t set, instantiate a new XMLHttpRequest object, open the request, set the appropriate response type, and send data as appropriate. In abort, we simply call abort on the jqXHR object passed to the transport factory function.

So where is this useful? You remember our FileReader/XHR2 plugin, right? This is a complement (and partial replacement, if desired) to that – now we can receive blobs/arraybuffers from the server, as well as send them up, all within the comfortably familiar jQuery interface. Neat, huh?

But wait! The plugin gives us feedback on upload progress! Can we do that with this ajax transport?

Of course! Take a look at our xhr2 plugin post again, and you’ll see where we’re listening in on the ‘progress’ event on our XMLHttpRequest object. The relevant handler function looks like:

DeferXhr.prototype.uploadProgress = function(event){
        if (event.lengthComputable)
        {
            this.progress = (event.loaded/event.total);
            if (this.options.chunked)
            {
                this.progress *= (this.end/this.file.size);
            }
            this.time.end = +new Date();
            this.time.speed = (this.file.size*this.progress)/(this.time.end-this.time.start)*1000;
            console.log('time:', this.time.end-this.time.start, 'speed:', this.time.speed);
            this.defer.notify({state:Hup.state.FILE_UPLOAD_PROGRESS, file_name:this.file.name, speed:this.time.speed,
                progress:this.progress});
        }
    };

By adding a similar handler function with our ajax transport factory on the progress event, we can make calls to jqXHR.notify to send progress events and data to any listening .progress handlers dangling from our $.ajax call.

This, however, I leave as an exercise for the reader. 🙂
If you like, please feel free to push the relevant changes to the gist in question.

Here’s the promised code:

Christopher Keefer

Christopher Keefer

Christopher Keefer is a Senior Software Engineer at Art & Logic. He generally spends his spare time on the computer too, so there isn't much hope for him.
Christopher Keefer

Latest posts by Christopher Keefer (see all)

Tags:

Creative Commons License

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

5 Comments

  1. Nir Soudry

    Very nice stuff!
    A thing to note, XMLHttpRequest doesn’t support responseType changes for sync calls anymore.

  2. Daniel

    Line 37:
    “options.async || true” will always set async = true, even if the options.async = false.

  3. Daniel

    I’m trying to use the “$.ajaxTransport….” snippet to simulate an ArrayBuffer that I can use to download images. This is required because IE versions prior to 11 don’t implement the xhr.response property. The problem is that what I get after the jQuery.ajax call is not an ArrayBuffer but a raw chunk of bytes. I tried to create a binary, then a base64 representation of that chunk with something like this:

    var i = dataBytes.length;
    while (i–)
    {
    binaryString[i] = String.fromCharCode(dataBytes.charCodeAt(i) & 0xff);
    }
    var base64 = window.btoa(binaryData);

    But the binaryString and subsequently the base64 are not correct so no image is rendered.

    Do I need to implement some additional functionality to process that raw chunk of bytes?

  4. Thibaut (@w3blogfr)

    One more think. If you want to send your data with other content-type.

    Please add :

    if(options.contentType){
    xhr.setRequestHeader(“Content-type”,options.contentType);
    }

  5. Mark

    Hi!
    I don’t know how integrate handler function with ajax transport factory for the progress event.

    Can you send me the complete code of AjaxTransport factory (and dependencies)?

    Thank you very much!

Contact Us