Caching and MIME Types in Rails

Favorite question after every presentation: but does it scale? Now, I don't want to get into a philosophical debate over 'scaling' and what that entails, but like it or not, it is something that needs to be considered at least from a distance. Keeping this in mind, as soon as I delved into caching in Rails, I quickly realized that MIME types are a great source of headaches. Namely, serving any non-HTML type (XML, etc.) often creates a number of unwanted problems.

Apache workaround for page_cache

Full out page caching is blazing fast since once the page is generated, no part of the Rails system sees that request until the page is expired. In theory, your load capacity in this instance is only limited by the ability of your web front-end (apache, lighty, etc) to serve static content. However, if you decide to cache an XML feed, Rails will dutifully produce an .html file which your web-server will serve as exactly that: HTML. Obviously, and as others have discovered, that's not what we want. An example is in order:

class RssController < ApplicationController
  layout nil
  session :off

  caches_page :syndicate

  def syndicate
    @data = Entry.find(:all, :order => "pubdate desc", :limit => 10)

    # Render a RSS 2.0 compliant Builder template
    # MIME type will be automatically set to text/xml when generated by Rails
    render(:action => 'rss2_feed')
  end
end

Mike Zornek documented this MIME inconsistency problem, and a couple of commenter's proposed a nice solution: override the type in your apache config! Specifically, Location and FilesMatch directives were suggested. However, after some snooping around, I came up with a slightly different solution:

# In your VirtualHost directive, include:
<LocationMatch "/rss">
  ForceType text/xml;charset=utf-8
</LocationMatch>

Instead of matching a specific location this will override the MIME type to be text/xml on any path that begins with an /rss prefix. I like to keep my controllers and routes separate, and this proved to be an easy and an effective solution to fix the problem.

Forcing MIME types in cache_fu (Memcached)

Memcached has rightfully earned the status of a high-performance, distributed memory object caching system. It is by far the most widely used system out there, and thanks to Chris Wanstrath's plugin (cache_fu), Rails sports a spectacular interface for it!

Having said that, while it is exceptionally easy to setup, it is subject to the same MIME type problems. Hence, I got my hands dirty and came up with a small patch for fragment_cache.rb to enable the following behavior:

class RssController < ApplicationController
  layout nil
  session :off

  # Use memcached, expire in 5 minutes, and serve as XML: ---vvvvvvvvvvvvvvvvvvvvvv---
  caches_action :syndicate => { :ttl => 5.minutes, :content_type => 'application/xml'}

  def syndicate
    @data = Entry.find(:all, :order => "pubdate desc", :limit => 10)

    # Render a RSS 2.0 compliant Builder template
    # MIME type will be automatically set to text/xml when generated by Rails
    render(:action => 'rss2_feed')
  end
end
fragment_cache.patch - Run: patch < fragment_cache.patch

You can explicitly set the content_type and completely override all Rails MIME handling behaviors. Of course, omitting the key defaults back to Rails, hence you should have more flexibility and still keep the best of both worlds!


Ilya Grigorik

Ilya Grigorik is a web performance engineer and developer advocate at Google, where his focus is on making the web fast and driving adoption of performance best practices at Google and beyond.