Your friends at Viget present Extend, a Code & Technology Blog

Rack Support in Rails: Why It Matters

As I’ve mentioned elsewhere, my RailsConf session on Rack support in Rails went very well. I’m not planning on giving this talk again, however, so I thought I’d write a quick summary of it for people who are interested in a little more detail than the slides can provide.

My main motivation in giving this talk was to call attention to what I feel has been the most important change in Rails over the past year – its move to become Rack-compatible. The initial work on this was done by Ezra almost a year ago, but it only made it into a stable release with 2.3. This delay meant that it stayed a bit under the radar, and even the addition of Rails Metal didn’t really pull it into the spotlight. Then, in December of 2008, the announcement of the Merb-Rails merger pretty well decided the story of the year for Rails.

So What?

So, why is Rack support so important? Why should it be recognized as more vital to the success of Rails than the merger of the two most popular Ruby web frameworks? Rack is just a common API for Ruby web frameworks, after all, and as such isn’t as flashy as, say, Merb’s router.

The answer is just that Rack is a common API for Ruby web frameworks. It’s essentially the Ruby version of Python’s WSGI, specifiying a simple interface for handling HTTP requests in Ruby. The simplest Rack-compatible web application would look something like this:

class SimpleRackApp
  def call(env)
    [?      
      status,  # 200
      headers, # {"Content-Type" => "text/html"}? 
      body     # ["..."]?
    ]?
  end
end

To be Rack-compatible, an application need only have a call method that accepts a hash of environment data (including, for instance, the HTTP headers included in the request) and returns an array of an HTTP status, a hash of headers, and a body that responds to .each.

Standardizing the interface for web applications means that you can link them together in interesting and useful ways – leading us to Rack middleware. Middlewares are just Rack-compatible applications that intercept and manipulate a request or response; a middleware could, for example, rescue any unhandled exceptions from your application and display a friendly error page.

Rack in Rails

As one of the results of the integration of Rack into Rails, many of the functions of ActionController have been refactored into middlewares, which means they can be required into non-Rails applications and used. As of today, these middlewares live in the ActionDispatch module, and include classes like ActionDispatch::Failsafe, ActionDispatch::ShowExceptions, and ActionDispatch::ParamsParser (among others). Normally, these will be automatically required when you run your Rails application through script/server or Passenger, but you can control their inclusion or replace them entirely with environment.rb; see the Rack guide for more details.

Rails Metal is another of the results of the move to Rack. Metal provides a way to bypass some of the normal overhead of the Rails stack, allowing a developer to specify certain routes and code to execute when those routes are hit. Metal actions avoid the entirety of ActionController, and so are somewhat faster than standard Rails actions (though this performance benefit may not always be significant).

Tools and Techniques

All of this is just background, though; the real argument in favor of the importance of Rack is found when you look at what you can now do. In the session at RailsConf, I presented two such examples: Rack::Bug and progressive caching. Rack::Bug is a middleware, and some great things have been written on it recently. Progressive caching is a technique that uses Rails Metal to respond to AJAX requests, making page caching a more generally useful strategy (it is also one of the other talks I give, and I’ve written on it in a couple of places).

During the final part of the section, I showed how to write a couple of middlewares that might be generally useful: Rack::Embiggener and Rack::Hoptoad. The latter is obviously a port of the hoptoad_notifier plugin to a Rack middleware (and indeed, this is a direction that the thoughtbot team is considering – something I’m really looking forward to!). The former, however, may be a more interesting example. Rack::Embiggener is a middleware that intercepts the response from a Rack application, detects any TinyURLs embedded in it, and replaces them with the expanded URL. An obvious use-case for this is Twitter, where tweets could be stored with TinyURLs in place (remaining under the 140 character limit), while they are displayed with the full URL visible regardless of the length1.

module Rack
  class Embiggener
    def initialize(app)
      @app = app
    end
    
    def call(env)
      status, headers, body = @app.call(env)
      headers.delete('Content-Length')
    
      response = Rack::Response.new(
        Rack::Embiggener.embiggen_urls(body),
        status,
        headers
      )
    
      response.finish
      return response.to_a
    end
        
    def self.embiggen_urls(original_body)
?     new_body = []
      
      original_body.each { |line|
        tinys = line.scan(/(http:\/\/(?:www\.)?tinyurl\.com\/(.{6}))/)
  ?     new_body << tinys.uniq.inject(line) do |body, tiny|
    ?     original_tiny = tiny[0]
    ?     preview_url = "http://preview.tinyurl.com/#{tiny[1]}"
    ?     response = Net::HTTP.get_response(URI.parse(preview_url))
    ?     embiggened_url = response.body.match(/redirecturl" href="(.*)">/)[1]
    ??    body.gsub(original_tiny, embiggened_url)
  ?     end
?     }
?     
      new_body
?   end
  end?
end

Walking through the code:

  • #initialize stores the Rack application object in a variable for later use.
  • #call passes the request to the application stored in @app, and records its responses. It then deletes the Content-Length header (since we’ll be enlarging some URLs), and creates a new Rack response with the same information, passing the original body through the embiggen_urls method. The response.finish call ensures that the appropriate headers (including a new Content-Length) are set.
  • embiggen_urls accepts a body (that, per the Rack specification, responds to .each), and (inefficiently) scans over it to find and replace the TinyURLs2.

With this middleware, then, any Rack-compatible application – be it Twitter, some other Rails 2.3 application, a Sinatra microapp, or even something written in Camping – can replace TinyURLs with their expansions.

1 More interestingly, Twitter or another service might be able to replace the full ‘http:/tinyurl.com/[foo]’ with just ‘[foo]’ and a single-character flag of some sort, providing even more storage savings.

2 This code was just meant to inspire people by showing something that could be done, and isn’t meant to be particularly efficient or good. Feel free to fork and improve it!

Wrapup

One of my favorite sayings comes from Socrates: “A list is not a definition.” My presentation (and this post) argue for the importance of Rack in Rails, but all they really do is give a list of examples of useful tools and techniques. My final point, then, is this: Rack compliance makes Rails a first-class member of the community of Ruby web frameworks (contrast that with Django, which implements a non-standard version of WSGI). This means that work done for Rails, such as the refactoring of parts of ActionController into ActionDispatch’s middlewares, benefits both Rails developers and users of other frameworks, and vice versa. Rack unites the fragmented Ruby web framework ecosystem, which means that no matter the community in which we work, our efforts improve the whole rather than just a single piece. As we’ve seen in open source time and again, the more people working on a set of problems, the better.


Get More From Viget

Subscribe to get our monthly newsletter and occasional special announcements.