Rails and Content Negotiation Revisited

Let’s revisit the subject of content negotiation in Rails. Now that I’ve upgraded to Rails 1.1 I’ve had a chance to play with the new responds_to functionality. To
get some feedback, I also posted a comment to Jamis’ blog and a message to
the REST microformat list.

My conclusion is that adding content negotiation to Rails would be valuable. The new responds_to functionality is useful when you want to override the default behavior. Having content negotiation would be useful for making the default behavior more intelligent.

Templates already work in a similar manner. For example, say you write this code:

class MyController < ApplicationController
  def list
  end
end

Rails will assume that you want to render a template called list.rhtml. However,
you can override this behavior as needed like this:

class MyController < ApplicationController
  def list
    render(:template => 'not_the_default_template')
  end
end

Content negotiation would work in a similar manner. It would load the appropriate
template based on a client’s request as specified in the HTTP accept header. You
can then override that behavior when needed using responds_to.

The way I propose this works is that Content Types are mapped to templates via their file extensions. For instance:

Content Type
Template Name

text/html
list.rhtml

application/xhtml+xml
list.rxhtml

text/javascript, application/javascript
list.rjs

application/atom+xml
list.ratom

application/atom+rss
list.rss

application/x-yaml
list.ryaml

One key thing to understand – the template extension does not dictate the template type. Thus, list.rxhtml could be an ERB template or a Builder template or any other sort of template.

Right now Rails doesn’t support such a scheme because template extensions are hard-coded to template types. This is done in ActionView::Base::create_template_source:

def create_template_source(extension, template, render_symbol, locals)
  if template_requires_setup?(extension)
    body = case extension.to_sym
    when :rxml
      "xml = Builder::XmlMarkup.new(:indent => 2)\n" +
      "@controller.headers['Content-Type'] ||= 'application/xml'\n" +
      template
    when :rjs
      "@controller.headers['Content-Type'] ||= 'text/javascript'\n" +
      "update_page do |page|\n#{template}\nend"
   end
  else
    body = ERB.new(template, nil, @@erb_trim_mode).src
  end
  
  @@template_args[render_symbol] ||= {}
  locals_keys = @@template_args[render_symbol].keys | locals
  @@template_args[render_symbol] = locals_keys.inject(
                                    {}) { |h, k| h[k] = true; h }
  locals_code= ""
  locals_keys.each do |key|
    locals_code << "#{key} = local_assigns[:#{key}]
                         if local_assigns.has_key?(:#{key})\n"
  end
  
  "def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend"
end

It would be easy to work around this code since the template parameter includes
the full source of the template. With a little regular expression magic, you could figure out what type of template has been loaded and then run the appropriate code.

However – what if we did something more clever? What if every template started
with an comment that specified its type – similar to a Unix shebang or XML processing instruction. Maybe something like this:

For ERB:
<!-- template_type :erb -->

For RJS:
# template_type :rjs

For Builder:
<!-- template_type :erb -->

It would then be easy to extract out the template type when it was loaded. If the template comment did not exist (i.e., for all existing templates) then you could revert to regular expression magic.

This solution has one very nice property – it would make it really easy to add additional template types to Rails (not that its that hard now).

To recap, the idea is:

  • Implement default content negotiation by mapping content types to template names via file extensions (.rhtml, .rxthml, .rjs., .ratom, etc.)
  • Add code to Rails so that it determine a template’s type when the template is
    loaded
  • Use responds_to to override the default behavior as needed.

What do people think of this idea? If there is interest, I’ll update my content
negotiation plugin and add this functionality to Rails 1.1.

  1. March 31, 2006

    I must admit I haven’t yet worked out what advantage of content negotiation has over a query parameter (e.g. the ubiquitous “?type=xml”.

    The huge advantage for query paramaters is they are so much easier to manipulate and explore e.g. it’s trivial to look at an XML dataset in a browser, or grab the data with curl. I know it’s possible to add a custom header using curl, but it’s still more work and less intuitive than appending “?type=xml” to the url on the command line.

    Reply
  2. Dr. Ernie
    March 31, 2006

    It is interesting, but I think the right place to discuss this is on the rails-core mailing list:

    http://www.ruby-forum.com/forum/16

    That way, you can socialize it with the people who really understand — and control — Rail’s guts.

    Reply
  3. Charlie Savage –
    March 31, 2006

    Jonno – yes, including a type parameter in the url is a common hack as is including an action parameter. In fact, I’ve done both of those myself. And like you say, the reason is that browsers don’t give you access to the HTTP Accept header, at least not in a dynamic way (i.e., via JavaScript).

    Anyway, I think it depends on what you are trying to accomplish. If getting an xml representation of a resource is what you really want, then putting type=xml makes sense. On the other hand, if you want to generically identify some resource and have the server return the appropriate content for the client, then it makes sense to use HTTP’s built in mechanisms for this. Might want to take a look at this [thread](http://groups.yahoo.com/group/rest-discuss/message/5168) on the Yahoo rest-discuss list.

    Reply
  4. Charlie Savage –
    March 31, 2006

    Dr. Ernie – Yes, good point.

    Reply
  5. Peter Williams
    March 31, 2006

    Jonno, you can do still use curl to play, even with content negotation in place. For example, `curl –header “Accept: application/atom+xml” -i http://somesite.example/feed` would get you the atom version of the resource if the host supported content negotiation.

    Of course, that does not help with javascript but it still fun.

    Reply
  6. April 2, 2006

    File extensions are used to give the content type of a file.

    On the web, they were sacrified to “pretty-url”. It may be simpler to restore them as a content-type indicators than using the “&type=xy” hack or ask for the browser to ask for XY content-type.

    Reply
  7. Charlie Savage –
    April 3, 2006

    Zimbatm – The problem is that you tie yourself to a specific implementation. For example, say you have a link that is http://mysite/home.asp. Later you decided to use php. Now you are foced to change the link and break people’s bookmarks. One of the wonders of the web and http is that you can move a site to a different computer running a different operating sytem running a different web server and clients never will notice. Don’t do anything that limits this very valuable flexibility.

    Reply
  8. April 5, 2006

    Just replied to your original post. I must’ve missed this in my newsreader.

    I agree with the idea, the only thing I don’t really like is the “magic” about deciding how to process the template based on a comment. If you do decide to do this I think the default behaviour (should you not be able to find the comment) would be the safest option: just do what rails does and treat it like an rhtml file and process it with ERB.

    Other than that I love the idea and I would encourage you give it a try.

    Reply

Leave a Reply

Your email address will not be published.

Top