The Life of a Radar

How Rails Works #2: Mime Types & respond_to
25 Apr 2009

THIRD COPY This guide is available on Github

A Refresher

respond_to is a method that defines different formats that your actions, well, respond to. For those who are unfamiliar with respond_to or simply forgot about it, here's a refresher.

Take this example:

class BlogsController < ApplicationController
  def index
    @blogs = Blog.all(:limit => 10)
    respond_to do |format|
      format.html
      format.xml
    end
  end
end

It's an index action in a controller called BlogsController and we have a collection of Blog objects stored in @blogs. We then call respond_to and specify the block with a single argument of format. On format we call html and xml which will render app/views/blogs/index.html.erb and app/views/blogs/index.xml.erb respectively. In those templates we can iterate over the @blogs collection and do whatever with it that we fancy. There's a shorter way to write the respond_to for this:

respond_to :html, :xml

If we want some formats to respond one way, and some others to respond another way we can do this:

respond_to do |format|
  format.html { @blogs = Blog.all(:limit => 10) }
  format.any(:atom, :rss) { @blogs = Blog.all }
end

In this example index.html.erb's @blogs will contain 10 objects, where index.atom.erb's and index.rss.erb's @blogs will contain all the blogs record.

Diving In

My first post in this series was my Timezone Overview for Rails 2.2. Today I would like to cover how mime types and respond_to work in Rails 2.3. The reason I choose both of these instead of just respond_to is because they tie in close together.

I've tried to make all the methods in this guide clickable with links to the source on Github.

Firstly I'll talk about MIME types in Rails.

The default MIME types are loaded when Rails starts up by the final line in actionpack/lib/actioncontroller/mimetype.rb:

require 'action_controller/mime_types'

This loads the actionpack/lib/actioncontroller/mimetypes.rb file and registers the default MIME types:

Mime::Type.register "*/*", :all
Mime::Type.register "text/plain", :text, [], %w(txt)
Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript )
Mime::Type.register "text/css", :css
Mime::Type.register "text/calendar", :ics
Mime::Type.register "text/csv", :csv
Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
Mime::Type.register "application/rss+xml", :rss
Mime::Type.register "application/atom+xml", :atom
Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml )

Mime::Type.register "multipart/form-data", :multipart_form
Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form

# http://www.ietf.org/rfc/rfc4627.txt
# http://www.json.org/JSONRequest.html
Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )  

You may recognise the syntax used in mimetypes.rb from your app's config/initializers/mimetypes.rb. This file is used to define new MIME types for your application and is loaded on initialization of your application by this line in initializer.rb

load_application_initializers

and the corresponding load_application_initializers method:

def load_application_initializers
  if gems_dependencies_loaded
    Dir["#{configuration.root_path}/config/initializers/**/*.rb"].sort.each do |initializer|
      load(initializer)
    end
  end
end

which is responsible for loading all your application's initializers, including config/initializers/mime_types.rb.

The MIME types defined by Rails and your initializers are the methods that you call on the block object format or the symbols that you pass to respond_to. You may recognise these from the symbols passed to the Mime::Type.register calls earlier.

respond_to's code may look a bit intimidating at first but it's not really all that bad:

def respond_to(*types, &block)
  raise ArgumentError, "respond_to takes either types or a block, never both" unless types.any? ^ block
  block ||= lambda { |responder| types.each { |type| responder.send(type) } }
  responder = Responder.new(self)
  block.call(responder)
  responder.respond
end

If we use the block syntax this will just call the block object with the argument of responder which is defined as Responder.new.

If we use the single line syntax. this will just pass in the types as an array. This method then defines a lambda which is a Proc object just like a usual block. This takes one argument, called responder and then calls the types on the responder object.

The ||= on the block definition means "set this variable only if it hasn't been set already". The next line is defining the responder object which triggers initialize method for Responder:

class Responder
  def initialize(controller)
    @controller = controller
    @request    = controller.request
    @response   = controller.response
  
    if ActionController::Base.use_accept_header
      @mime_type_priority = Array(Mime::Type.lookup_by_extension(@request.parameters[:format]) || @request.accepts)
    else
      @mime_type_priority = [@request.format]
    end

    @order     = []
    @responses = {}
  end
  ...
end

This defines a couple of key variables, namely @mime_type_priority and @responses.

The first, @mime_type_priorty calls Mime::Type.lookup_by_extension(@request.parameters[:format]) which looks like:

def lookup_by_extension(extension)
  EXTENSION_LOOKUP[extension]
end

EXTENSION_LOOKUP is defined on line #5 of mime_type.rb as a hash:

EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }

What happens when a key is looked for on this hash it calls Mime::Type.new(key) which calls the initialize method for Mime::Type:

def initialize(string, symbol = nil, synonyms = [])
  @symbol, @synonyms = symbol, synonyms
   @string = string
end

We eventually understand that all our original @mime_type_priority is simply a Mime::Type object. If we requested HTML, the MIME type would be: #&lt;Mime::Type:0x2624380 @synonyms=["application/xhtml+xml"], @symbol=:html, @string="text/html"&gt;. This element is made into an array and then used in the respond method.

The second variable, @responses is defined as an empty hash. The magic happens in the respond method:

def respond
  for priority in @mime_type_priority
    if priority == Mime::ALL
      @responses[@order.first].call
      return
    else
      if @responses[priority]
        @responses[priority].call
        return # mime type match found, be happy and return
      end
    end
  end

  if @order.include?(Mime::ALL)
    @responses[Mime::ALL].call
  else
    @controller.send :head, :not_acceptable
  end
end

This method iterates over @mime_type_priority and then checks firstly if the element is a Mime::ALL This can be achieved by making the format of the URL "all", such as http://localhost:3000/blogs.all. If the element is not Mime::ALL this continues iterating through until it finds a format that is defined. It does this by checking if there is a key in the @responses hash... but it's empty. So I first thought, too. Buried just over halfway is this method which is called when the file is loaded and this calls generate_method_for_mime which does some funky class_eval'ing:

def self.generate_method_for_mime(mime)
  sym = mime.is_a?(Symbol) ? mime : mime.to_sym
  const = sym.to_s.upcase
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def #{sym}(&block)                # def html(&block)
      custom(Mime::#{const}, &block)  #   custom(Mime::HTML, &block)
    end                               # end
  RUBY
end

Remember back when we were calling responder.send(type)? This is what it calls, this generated method.

The generated methods take an optional block, as shown by the code for custom:

def custom(mime_type, &block)
  mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s)

  @order << mime_type

  @responses[mime_type] ||= Proc.new do
    @response.template.template_format = mime_type.to_sym
    @response.content_type = mime_type.to_s
    block_given? ? block.call : @controller.send(:render, :action => @controller.action_name)
  end
end

If the first argument given to custom is not a Mime::Type object then it will do a lookup and find it. The method will then concatenate into @order mime_type. This variable is used for when Mime::ALL is specified as the mime type, it will use the first one in this list, which will be the first one you've called in your respond_to. If you've define your respond_to like this:

def index
  @blogs = Blog.all(:limit => 10)
  respond_to do |format|
    format.html 
    format.json { render :json => @blogs }
    format.any(:atom, :xml) { @blogs = Blog.all }
  end
end

The all will call html because it was defined first. Following on with this example, the format.html method isn't passed a block, but the format.xml is. This determines what this line does in custom:

block_given? ? block.call : @controller.send(:render, :action => @controller.action_name)

When we don't pass a block for the format.html it will render the action, looking for index.html.erb which, in the default scaffold, will list all the blogs. Rails does this by calling:

@controller.send(:render, :action => @controller.action_name)

It calls send because render is a protected method. This passes a single argument to render, { :action => @controller.action_name } which in this example is "index". default_template:

def default_template(action_name = self.action_name)
  self.view_paths.find_template(default_template_name(action_name), default_template_format)
end

This calls a number of methods, but what we're interested in here firsty is the default_template_format method. This method looks like this:

def default_template_format
  response.template.template_format
end

and template_format looks like:

def template_format
  if defined? @template_format
    @template_format
  elsif controller && controller.respond_to?(:request)
    @template_format = controller.request.template_format.to_sym
  else
    @template_format = :html
  end
end

Our request will match the elsif in this code which will call another template_format method, this time on the request object of our controller. This method looks like this:

def template_format
  parameter_format = parameters[:format]
  if parameter_format
    parameter_format
  elsif xhr?
    :js
  else
    :html
  end
end

Yes! After all this time it does call parameters[:format]! This is also known as params[:format] and, to the well trained "html". So way back in our initial default_template call:

def default_template(action_name = self.action_name)
  self.view_paths.find_template(default_template_name(action_name), default_template_format)
end

We were firstly interested in default_template_format which is what we just covered. Now we're interested in find_template:

def find_template(original_template_path, format = nil, html_fallback = true)
  return original_template_path if original_template_path.respond_to?(:render)
  template_path = original_template_path.sub(/^\//, '')

  each do |load_path|
    if format && (template = load_path["#{template_path}.#{I18n.locale}.#{format}"])
      return template
    elsif format && (template = load_path["#{template_path}.#{format}"])
      return template
    elsif template = load_path["#{template_path}.#{I18n.locale}"]
      return template
    elsif template = load_path[template_path]
      return template
    # Try to find html version if the format is javascript
    elsif format == :js && html_fallback && template = load_path["#{template_path}.#{I18n.locale}.html"]
      return template
    elsif format == :js && html_fallback && template = load_path["#{template_path}.html"]
      return template
    end
  end

  return Template.new(original_template_path) if File.file?(original_template_path)

  raise MissingTemplate.new(self, original_template_path, format)
end

Nothing inside the each block will match, so it will not return anything. Instead, what is returned is the line after the each block:

  return Template.new(original_template_path) if File.file?(original_template_path)

This returns a Template object like this:

#<ActionView::Template:0x2193258 @format=nil, @base_path=nil, @template_path="blogs/", @extension=nil, @locale=nil, @name=nil, @load_path=nil>

render_for_file takes this object and uses it to render the appropriate template that the user requested.

When we do pass a block to our format.xml, this is much simpler in the call to render:

elsif xml = options[:xml]
  response.content_type ||= Mime::XML
  render_for_text(xml.respond_to?(:to_xml) ? xml.to_xml : xml, options[:status])
end

This code calls to_xml on our collection and converts it to an XML document.

Finally, the any is another method defined on the Responder object. You can pass to this a collection of mime types and for these mime types it will perform the block. In this example we've used:

  format.any(:atom, :xml) { @blogs = Blog.all }

but you can also do:

  format.any { @blogs = Blog.all }

which will define the response for all currently undefined mime types.

any goes a bit like this:

def any(*args, &block)
  if args.any?
    args.each { |type| send(type, &block) }
  else
    custom(@mime_type_priority.first, &block)
  end
end 

We've passed args to ours, so it'll just call the methods atom and xml on the Responder object and then render the appropriate views for these.

If we don't pass arguments to any, any MIME type that we don't have a respond_to line for will call the any block instead. Take for example, if we had this:

def index
  @blogs = Blog.all(:limit => 10)
  respond_to do |format|
    format.html 
    format.json { render :json => @blogs }
    format.any { @blogs = Blog.all }
  end
end

And we requested an XML page, http://localhost:3000/blogs.xml, this will trigger the code in the any block to be ran.

So that wraps up this guide on respond_to. Thanks for reading!

blog comments powered by Disqus