So at my day job we're converting the front end of our product from a Java thick client to a Struts 2 / JSP / several-other-buzzwords webapp. We officially support three browsers (in no particular order other than my blatant preference): Firefox, Safari, and Internet Explorer. We're using a fair amount of Ajax via Prototype and Scriptaculous. Again, no big surprise.
One of the problems we've encountered with our Ajax stuff is that anything we return from a normal Ajax call is subject to caching by the browser. Now I know the standard tricks to defeat caching are to a) submit the request as a POST; and 2) submit as a GET, but append a random string (e.g., a timestamp) to the request string to force the browser to fail on the cache lookup.
Unfortunately, being the curmudgeonly programmer and language maven that I am, I object to both of these approaches. Sometimes (most times) GET is the proper verb to use for my request; it's not changing state on the server, just retrieving it. POST should be used for changing state. The random string hack is just that -- a hack. If the browser is going to cache the results of a single request, it's probably going to cache the result of each "unique" request we create by tacking on this otherwise-irrelevant snippet of "data". I don't want all that unnecessary cruft in my customers' caches if I can help it.
There must be a better way.
Fortunately, there is a better way as of HTTP 1.1, in the form of the
Cache-Controlresponse header. See Section 14.9 of the HTTP 1.1 spec for all the gory details. Suffice it to say, including a response header of
Cache-Control: no-cachewith all of our Ajax responses worked to defeat caching of our Ajax-GET snippets.
Except on IE.
Are you surprised? I was, but only because
Cache-Control: no-cacheactually works on IE, to a point. So it wasn't obvious at first that IE was screwing up. Fortunately, we have the world's best quality engineering team, so they caught the problem when we lowly developers weren't seeing it.
Long story short: it turns out that (in our application at least; YMMV) the
Cache-Control: no-cacheresponse header actually works to prohibit caching in IE. Until the response is > ~8K in size. At which point IE caches it anyway. Which leads to all kinds of fun "but I saved my edits, I know I did" debates between QE and development when the browser returns the cached results instead of the latest and greatest.
This is where Prototype comes in. You see, Prototype already has a set of headers it adds to every Ajax request submitted through it. For example, it sets the header
XMLHttpRequest. We use this in our app to distinguish between Ajax GETs and non-Ajax (NAjax?) GETs. Comes in handy sometimes. (Note: See Prototype's source file
prototype.js, specifically the
Ajax.Request.setRequestHeaders()function for the details on this; ~ line 1241 in Prototype 126.96.36.199.)
So the trick is to get Prototype to include another header with its Ajax requests:
If-Modified-Sinceto some datetime in the past, and the browser should always go to the server instead of the (expired) cache. I could do this by hacking my prototype.js file to include this new header in
Ajax.Request.setRequestHeaders(), but then I'd have to remember to re-hack every time I upgrade Prototype. Not fun. I could also submit it as a patch, I suppose, but I don't know that everyone needs this behavior by default.
Enter The Next Best Thing: Functional Programming! Prototype has a wonderful FP-ish function called
wrap(), which is defined on
Element.Methods(prototype.js, ~ line 1652 in 188.8.131.52).
wrap()lets me extend the existing
setRequestHeaders()function from the outside thusly:
Ajax.Request.prototype.setRequestHeaders =In case this isn't clear, I'm replacing the original definition of the
// do my stuff first; e.g., set 'If-Modified-Since'
original() // then call the original version
setRequestHeaders()function with a new (anonymous) function that does my stuff first, then does whatever the original was defined to do. To Java programmers circa 2004, this looks kind of like "before advice" in Aspect-Oriented Programming. Without the new-language-compiler-tools-runtime-stack baggage. To functional programming types, it looks like function composition. Without the lambdas and general my-thesis-is-bigger-than-yours snootiness.
To me it looks like a pretty nifty hack, one that plays by everyone's rules.
Well. Except IE's.
But I'm okay with that.