Wednesday, 20 June 2007
The State of Proxy Caching
A while back I wrote up the state of browser caching, after writing a quick-and-dirty XHR-based test page, with the idea that if people know how their content is handled by common implementations, they’d be able to trust caches a bit more.
The other half that they need to know about, of course, is proxy caching; depending on who you listen to, somewhere between 20% and 50% of clients on the internet are behind some kind of proxy, and since they’re operated on behalf of a network provider, rather than the publisher or end user.
So, last northern summer (yes, it’s taken that long to write this up; sorry) we tested a selection of Open Source and commercial proxy caches with the ever-so-useful Co-Advisor test suite, which is pretty much the industry standard. Each was tested in its “out-of-the-box” state; any device can be configured to do bad things, but generally administrators just switch them on.
Altogether, eight different implementations were tested. I can’t report exactly which device did what, because of the EULA restrictions on many commercial implementations. That’s OK; my goal isn’t to point fingers at any particular vendor, but rather to give people an idea of how their content will be treated once it gets onto the open Internet.
The good news is that the basics of URIs, HTTP connection handling and caching were not a problem; every implementation passed them with pretty much flying colours. When you send Cache-Control: no-cache or max-age, they’ll do the right thing, and generally they’ll parse the headers, forward them on, and return the response correctly.
The bad news is that more complex functionality is spottily supported, at best. I suspect this is because everyday browsing doesn’t exercise HTTP like more advanced uses like WebDAV, service APIs, etc.
See below for the details of the ups and downs. These are just the highlights; if you have more specific questions, raise them in comments and I'll do my best to answer.
Every implementation was able to handle 1024 byte long request URIs, but only a few were configured to allow 8192 bytes. It’d be interesting to see what support was like around 4096 bytes, but for the time being it’s probably safe to limit your URLs to 2k or less.
GET, HEAD, POST, PUT, DELETE, OPTIONS, and TRACE all seemed to work OK, but quite a few caches had problems with extension HTTP methods. If you’re using non-standard HTTP methods (or even some of the more esoteric WebDAV methods; there are a lot of them), beware.
As discussed previously, many implementations had problems with incorrectly-formatted Expires headers; the correct thing to do was to consider them to be stale (i.e., expired in the past), but most implementations tried to guess what the ill-formatted date meant — sometimes incorrectly. If you write your own Expires headers, be very careful.
Most of the CC directives were honoured with no problem; e.g., max-age, no-store, private, must-revalidate and proxy-revalidate were all treated correctly, even when there was a conflicting Expires header present. The only standout was s-maxage; for smoe reason, many implementations had a problem correctly revalidating responses with this CC directive.
Extension Cache-Control directives (e.g., Cache-Control: max-age=60, foo=bar) seem to be handled correctly by all implementations; that is, they’re ignored. Older versions of a venerable Open Source cache do sometimes incorrectly parse such headers (e.g., Cache-Control: foo="max-age=8" is interpreted as Cache-Control: max-age=8), but this is (hopefully) a pathological case.
One other thing to note; both the private and no-cache Cache-Control directives give you the option of listing some headers after them; e.g.,
with the intent being that this refines the semantics of that directive to apply just to those headers. In practice, implementations just ignore responses carrying these refinements, making them effectively uncacheable.
Validation was good in the simple cases, but tended to fall down in more complex circumstances, especially in situations with weak ETags, If-Range headers and other not-so-common things.
Caches are required to be updated by the headers in 304 responses, as well as responses to HEAD. For example, if you send a new Cache-Control header back with a new max-age value on one of these responses, the cache should replace the old value with the new one.
In practice, updates were spotty; a lot of the time, the test suite couldn’t get the cache into a state where it could tell, but when it could, there were failures. As a result, it’s probably not a good idea to rely on 304 responses or HEAD requests to update headers; better to just send a 200 back with a whole new representation.
Sadly, one of the most useful parts of the caching model, invalidation by side effect, isn’t supported at all. A few implementations would invalidate the Request-URI upon a DELETE, and even fewer upon PUT and DELETE, but that’s it. As a result, it’s harder to take full advantage of the cache, because you’ll have to mark things as uncacheable if you care about changes being available immediately.
Generally, header parsing was quite good; every implementation was able to parse simple headers, forward them as appropriate, and preserve order if there was more than one instance of a single header. The only thing that really tripped them up was HTTP’s support for spreading a single header across multiple lines, like this;
Cache-Control: max-age=60, public, must-revalidate
From what I saw, the most common mistake was when an implementation would try to support multi-line headers, but mess it up by removing the whitespace between lines (it should be preserved). In the real world, this shouldn’t be an issue, because no-one I’ve seen generates multi-line headers. Still, if you’re tempted to, don’t.
Another issue was a propensity for a few implementations to forward hop-by-hop headers (e.g., those listed in the Connection header, plus a few pre-defined ones like Trailer, TE, Upgrade). That’s a no-no, but it shouldn’t affect most publishers.
Of the implementations that supported chunked encoding (i.e., called themselves HTTP/1.1), most did a pretty good job. The only noticeable exception is when there’s both a Content-Length header and chunked encoding present; although HTTP forbids this situation, some servers may do it anyway, and it caused a few problems. Likewise, chunked requests had more than their share of hiccups, probably because they’re not very widely seen.
I was genuinely surprised to see that trailers don’t completely mess up most of the implementations; while there were some bugs, most of the tests passed. Go figure. Again, this shouldn’t affect real people.
There wasn’t any evidence for pipelining support, at all. A shame, but it’s not well-supported in browsers either.
Expect/Continue is a very useful facility that allows a client to figure out whether the server will take a request, based upon the headers, before sending the whole body. So, it’s a shame that the implementations tested supported it so spottily. The very simplest tests were passed by all comers, but I wouldn’t be comfortable recommending use of Expect/Continue on the open Internet today.
The Warning header is almost never generated by implementations, as far as I saw; disappointing. Don’t rely on getting warnings from caches about stale responses; it’s better to figure it out yourself (e.g., by examining the Age header). Also, don’t rely on intermediaries deleting Warning headers as directed by the RFC; only one implementation that I saw attempted this at all.