This post originated from an RSS feed registered with Ruby Buzz
by .
Original Post: Content Negotiation And Rails
Feed Title: cfis
Feed URL: http://cfis.savagexi.com/articles.rss
Feed Description: Charlie's Blog
Latest Ruby Buzz Posts
Latest Ruby Buzz Posts by
Latest Posts From cfis
Advertisement
Let's get back to REST
and Rails. One of the things Rails doesn't support is HTTP
Content Negotation. Why would you want this? Because different clients understand
different content types. For example, you can serve XHTML to Firefox but
only HTML to Internet Explorer. Or perhaps you are using AJAX, and want
to send JavaScript back to the browser. Or maybe another computer system
is talking to yours and it would like to have machine parseable XML thank
you.
To accomplish this in Rails 1.0 means manually setting the content type.
One place to do this is in a controller. But that doesn't seem right. Controllers
help guide a client request from its inception to its fulfillment. A controller
should have no knowledge about how the results are rendered. That's the
realm of ActiveView. Now you are talking! But wait...by the time a view
is invoked by Rails the content type has already been decided (that's not
quite true, but you sure don't want to be in an RHTML template generating
YAML).
So let's take a step back. We see that Rails has already started a custom
- HTML files are in .rhtml templates, XML files in .rxml, JavaScript in
.rjs. So let's stick with that. Thus, our goal is to implement code that
picks the appropriate template based on a client's stated desires as indicated
by the HTTP Accept header.
How do we such a thing? Well, first go read Joe Gregorios' excellent introduction to
the subject and implementation in Python. Then download this Rails plugin
that I wrote a few month ago that implements support for Mime Media
Types in Ruby and integrates them into Rails.
The key bit of code is the negotiate_content method. It iterates, in order
of importance as defined by the client, the different Mime Types supported
by the client. For each Mime Type, it sees if there is a corresponding
template with the right extension. Once it finds one, the template is loaded
and the request is fulfilled:
# Copyright (c) 2006 MapBuzz# Released under the MIT License# Author: Charlie Savagemodule ActionViewclass Basedefpick_template_extension(template_path)#:nodoc:if match = delegate_template_exists?(template_path) match.firstelsenegotiate_content(template_path)endenddefnegotiate_content(template_path) result ='rhtml'if!controller.respond_to?(:request)return result end negotiator = ContentNegotiator.new (controller.request.env['HTTP_ACCEPT'])# In order of preference, as specified by the client via# HTTP_ACCEPT, check to see if we can produce the media type negotiator.media_types.each do|media_type| extension = media_type.extensionif media_type.extension and template_exists?(template_path, extension) result = extension# Break out of this blockbreakendend# If all else fails return rhtml resultendendend
One thing that is not immediately obvious is that this code will run every
time a template or partial is run. We can use this our advantage.
For example, let's say you want to serve HTML to Internet Explorer and
XHTML to Firefox.
First, in your layout file do this:
<%= render(:partial =>'layouts/doctype')%>
Next, create one partial for Internet Explorer, called _doctype.rhtml:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd"><html><% controller.response.headers["Content-Type"] = MediaType::HTML %>
And now one for Firefox called _doctype.rxhtml (make sure to get the extension right!):
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><htmlxmlns="http://www.w3.org/1999/xhtml"xml:lang="en"><% controller.response.headers['Content-Type'] = MediaType::XHTML %>
Notice that we are neatly able to reuse 99% of our templates for both browsers,
yet at the same time include the <?xml
...?> header
for Firefox but not Internet Explorer.
Last, its good to see that the Rails team has recently discovered the
HTTP Accept header and is adding support in Rails 1.1. I haven't had a chance to
look at their implementation yet since I'm not running
Edge Rails. So take the following comment with a grain of salt. Its certainly
a good thing for Rails to become more aware of MimeTypes, but as mentioned above,
I don't think controllers are the right place to do this.
There is one caveat though. Mime
Types are a blunt instrument (xml/text doesn't tell you much about a document)
and therefore do not provide sufficient information in all cases. The most obvious
example is trying to serve the same content type either via a standard browser
request or an Ajax request. In the former case you generally want to render
a layout and in the later case you do not. Of course, if you are able to make Ajax
requests use a different content type (i.e., JavaScript) then there aren't
any issues. Alternatively, you can always implement two different external apis
to take care of this case.
Once I get my hands on Rails 1.1 I'll make sure to update this plugin if it is
still useful.
Also, I am interested in determining whether or not the browser that is accessing my site is of a media type = handheld - anybody have any ideas on how to do this?