Wednesday, October 30, 2013

MVC HTTP Headers for CDN caching

On a recent project I added upgraded our systems to use a Content Delivery Network (CDN). The project supplied content to users and we wanted to offload the 'heavy-lifting' of the content supply away from our PROD server onto a more robust delivery network.

The mobile clients would request the content from the CDN. If the CDN hadn't cached the content yet, it would grab it from our PROD server, cache it, and respond back to the clients. For this caching to work, though, some HTTP Header magic was required.

Which headers should you watch out for?

  • Cache control should be public
    • For the CDN to cache the content on behalf of your origin server, set the Cache-control=public
  • Expires Date
    • To stop the CDN regularly requested the same static content from your PROD server, set the Expires-Date setting to be in the far future (e.g. > 1 year)
  •  Vary 
    • Watch out for the 'Vary' header. MVC3 had initially defaulted to adding the "vary=*" http header which stopped the CDN from caching the data. 

A useful RFC to refer to is: RFC 2616 13.6:
A Vary header field-value of * always fails to match and subsequent requests on that resource can only be properly interpreted by the origin server.

.. or if you prefer the MSDN page: http://msdn.microsoft.com/en-us/library/system.web.httpcachepolicy.setomitvarystar.aspx

If you set Vary="NONE", this gets outputted as "Vary=*", which isn't very useful. Either get rid of the Vary header or set it to something broad such as "Vary: Accept-Encoding"

How to implement this in MVC?

[OutputCache(Location = OutputCacheLocation.Any, Duration = 31536000, VaryByParam = "")]
public ActionResult GetMyCDNContent(string someSortOfIdenfier)
{...
OutputCacheLocation.Any will be outputted as: Cache-Control: public
Duration = 31536000 will be outputted as:
  • Cache-Control: max-age=31536000
  • Expires: {a date a year from today}
  • VaryByParam="" will not output any Vary= header
  • This is what we needed; the content was going to be the same for all clients.

Here's how it ends up looking in Fiddler:






You might want something different if you have alternate caching needs for different encodings, such as support for gzip and non-gzip caching:
[OutputCache(Location = OutputCacheLocation.Any, Duration = 31536000, VaryByParam = "", VaryByHeader = "Accept-Encoding" )]
public ActionResult GetMyCDNContent(string someSortOfIdenfier)
{...

Here's how it ends up looking in Fiddler: