Use Sensible Long-Lived Cache headers

As some of you might expect, I watch all of my network traffic when I browse the web—you never know when you’ll see something interesting. This afternoon, for example, my curiosity was piqued when I noted that as I browsed around the Zune website, my browser issued conditional HTTP requests to revalidate some resources.

Microsoft sites are getting better and better at following best-practices for performance, including setting long-lived caching headers, so I was surprised to see a conditional request for this file. My first guess was that perhaps the site was sending a Vary header, which can cause unnecessary revalidations.

I was curious to find out what caching directives were sent on the original response, so I re-issued the Request unconditionally (use Fiddler’s context menu, or hit the U key) to avoid getting back the HTTP/304. I found that the original response didn’t include Vary and did include a Cache-Control header:

GET /xweb/lx/xap/Lynx.Silverlight.Controls.zip HTTP/1.1
Host: social.zune.net

HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Length: 1054539
Date: Mon, 25 Jan 2010 22:05:10 GMT
Content-Type: application/x-zip-compressed
ETag: "0a0d701a83ca1:0"
Cache-Control: max-age=4294880896
Last-Modified: Tue, 22 Dec 2009 15:18:24 GMT

Interesting. I flipped over to Fiddler’s “Caching” inspector and saw a note that the max-age header was “malformed” – this is incorrect, but it did immediately give me an idea about what was going on.

RFC2616 specifies that the max-age directive specifies a value in delta-seconds,which is defined as:

3.3.2 Delta Seconds
Some HTTP header fields allow a time value to be specified as an integer number of seconds, represented in decimal, after the time that the message was received.

        delta-seconds = 1*DIGIT

Notably, this definition says nothing about the size of the integer, nor does any of the verbiage around the max-age directive in general. However, the description of the Age header gives us a clue which suggests that the original authors of the spec expected a baseline of a 32bit integer:

Age values are non-negative decimal integers, representing time in seconds. If a cache receives a value larger than the largest positive integer it can represent, or if any of its age calculations overflows, it MUST transmit an Age header with a value of 2147483648 (2^31). An HTTP/1.1 server that includes a cache MUST include an Age header field in every response generated from its own cache. Caches SHOULD use an arithmetic type of at least 31 bits of range.

As you’ve likely guessed by now, Internet Explorer (or more specifically, WinINET, the HTTP stack below Internet Explorer) does not properly interpret the max-age=4294880896 value because it is greater than 2^31, the maximum value of a signed 32-bit integer. It thus treats this response as stale, despite the server’s intention to suggest that this resource will remain valid for 136 years. I also found that Opera 10.1 and Safari 4.0.3 make the same mistake, while Firefox and Chrome do not.

I’ve fixed the next version of Fiddler’s Caching inspector to treat values up to 2^63 as valid max-age values, and added a warning for any value over 2^31—sixty-eight years ought to be enough for anybody. :-)

Now, off to find a contact in the Zune team...

7/14/2010 Update: Improved in IE9; IE9 will accept any value up to 2^63 for the max-age value.

-Eric

Comments

  • Anonymous
    January 26, 2010
    The comment has been removed

  • Anonymous
    January 26, 2010
    The comment has been removed

  • Anonymous
    January 26, 2010
    Good to know. Do you know of similar limits on (unixtime representations of) dates in Expires and Set-Cookie (AKA the "2038 problem")? Anything over a year seems overkill.

  • Anonymous
    January 27, 2010
    Looking through the source code, ASP.NET won't let you set a max-age greater than one year (31536000 seconds): public void SetMaxAge(TimeSpan delta){
       if (delta < TimeSpan.Zero)   {
           throw new ArgumentOutOfRangeException("delta");
       }
       if (s_oneYear < delta)   {
           delta = s_oneYear;
       }
       if (!this._isMaxAgeSet || (delta < this._maxAge))   {
           this.Dirtied();
           this._maxAge = delta;
           this._isMaxAgeSet = true;
       }
    }

  • Anonymous
    February 01, 2010
    Was that a full day trip to the Zune Building... Good luck... Why not raise an issue ticket on connect.