If you’re one of that exceedingly rare breed who regularly check or subscribe to my blog, you probably want to give this post a miss. This one is more for people who find me through Google. A specific solution to a specific, geeky, problem.
Background
First a little scene setting…
Server side
I have a REST API that uses ETags for, amongst other things, concurrency control. That is, the version of an entity is (opaquely) identified by an ETag. You need to specify that ETag when you try and make any changes to that entity. If someone else changes the entity before you do, your ETag won’t match, so your update will fail, and you won’t unintentionally roll-back their change.
The REST API returns no content (HTTP 204) in response to a successful PUT request to edit an entity, and includes the new ETag representing the version of the updated entity.
Client side
I have a Dojo web tool that uses xhr.put to submit edits to the REST API. In order to make further subsequent edits to an entity without reloading the page, it stores the ETag that it gets back in the response header after every PUT.
The problem
In short, Internet Explorer. 🙂
In more detail, it seems that there is a bug in Internet Explorer when it gets a 204 no-content response to an xhr request from Javascript.
IE does drop whatever it uses to store the response content. Unhelpfully, it also drops all of the response headers.
This means my client side Javascript doesn’t get the updated ETag returned by the server.
It means my Javascript can make one update to the entity. After this, it doesn’t have the correct ETag required to make any subsequent updates.
This bug seems to be present on all versions of IE that I tried, including the latest-and-greatest.
A workaround
One way of handling this without needing to modify the REST API is to extend the behaviour of xhr.put so that, when running on Internet Explorer only, it follows up a successful PUT with a HEAD call to the same entity. This is just enough to get the updated response headers – to fetch the updated ETag that IE dropped from the PUT.
By daisy-chaining the two requests, this is invisible to code that calls xhr.put.
This is implemented as a module myproject/xhr
to use in place of dojo/_base/xhr
. It behaves in the same way as dojo/_base/xhr
, with the exception of this additional workaround.
define(["dojo/_base/declare", "dojo/_base/xhr", "dojo/_base/sniff", "dojo/_base/lang"], function (declare, xhr, sniff, lang) { function myXhr() { // // if we are using a JSON with ETags handler, then we are interested // in the the entity tag contained in the response header of the POST/PUT // requests // // however, on Internet Explorer, this response header is missing, // because of the bug described at http://www.enhanceie.com/ie/bugs.asp // (IE0013: IE XMLHTTP implementation turns 204 response code into bogus 1223 status code) // so we have to perform an additional GET to fetch the updated // entity tag. // // this bug is present on IE7-IE9 so we do this for any version of IE // this.put = function (request) { if (sniff("ie")) { return xhr.put.apply(this, arguments).then(lang.hitch(this, function(response){ delete request.headers['If-Match']; return xhr("HEAD", request); })); } else { return xhr.put.apply(this, arguments); } }; }; myXhr.prototype = xhr; return new myXhr(); });
Postscript
Oh IE, for crying out loud. How much more could we get done if we didn’t have to spend so much time working around your little quirks?!
Update (27/02/12): The comments below are worth a look. Adrian makes the very good point that this workaround is not appropriate for all situations.
Of course, the PUT + HEAD solution isn’t atomic. Given you are trying to use this for concurrency control the ETag you get back from the HEAD isn’t guaranteed to be the one that was lost from the PUT response. Depends on what your client is doing as to how much of a problem this could be.
This is true – I’m introducing a window (albeit a small one) where another client’s edit could be overwritten.
If I could be sure that my PUTs were putting a complete representation of the entity, then I could swap my HEAD for a GET and make sure that what I get back is the same as what I put.
Annoyingly, in this instance, I can’t.
As it is, it’s a trade off – weighing up the risk of two clients submitting different edits to the same entity at almost the same time possibly hitting this window, against the convenience of not requiring a page reload between edits.
Ultimately, a nicer “fix” may be for the REST API to user-agent sniff for IE and return a payload with the PUT response instead of no-content. Or for the client, when on IE, to add some query parameter to the PUT that requests the server API to include a payload body in the response. Unfortunately, API changes weren’t an option for me at the moment.
The best “fix”, as always, is the one where you use a better browser instead 😉