9780596521424
rack.html

Chapter 10. Rack

Rack is a simple specification and Ruby library for connecting web servers to web applications. The specification defines two main things: an environment, which is a hash that contains CGI-like headers describing the request; and a response, which is an array of values including the response code, response headers, and response body. When a server process receives an HTTP request, it constructs an environment hash and passes it to the application, which is then expected to return a response array.

Rack has become the standard for most Ruby web frameworks, including Rails. A massive amount of work has been done on the Rails framework to change it to adopt the philosophies and architecture of Rack. This has resulted in greatly improved organization and architecture of the Rails internals, as well as provided Rails developers with greater control of their applications.

The Rack library, in addition to supporting the Rack specification, provides tools for a lot of other common functionality needed by web applications. Things like routing URI paths to controllers in the application, parameter parsing, URI escaping, managing cookies, parsing file uploads, and much more.

This chapter will show you how Rack works on its own, and how you can make best use of it in your Rails applications.

Getting Started

A Rack application is a Ruby object that implements a call method that receives a CGI-like Rack environment hash as its argument and returns a Rack response. A Rack response is an array containing three things: a response code, headers, and a response body. The response body is an object, such as an array, that responds to the method each. The each method must yield the response body as strings when invoked. The simplest example is a Ruby object, such as a that returns an empty response body.

We're going to use Rack and the rackup tool for these examples, so you'll need to have the rack gem installed on your system. Mongrel is also a much better web server than WEBrick, so it is good to have that installed as well. Let's install both Rack and Mongrel in one go:

gem install rack mongrel

Unless you specify a handler for a particular web server, the rackup command will first try to use Mongrel, and if that isn't available, fall back to WEBrick. Read the output of rackup -h for details on all the available options.

Now let's create a simple application. Add the following code to a file named config.ru:

class RailsNutshellApp
  def call(env)
    [200, {'Content-Type' => 'text/plain'}, []]
  end
end

run RailsNutshellApp.new

Believe it or not we can actually run this as a fully working web application. The config.ru file is called a Rackup file, which we'll take a look at in a later section.

This example could also be implemented as a Ruby Proc object. A Proc object's given block is called using the call method:

run lambda { |env| [200, {'Content-Type' => 'text/plain'}, []] }

Now we'll use the rackup command to fire up a web server and start serving the application. By default, rackup will start the web server on port 9292. Feel free to pass the -p option to specify a different port. Let's start up the web server and make a request to the application:

$ rackup

You can also explicitly specify the name of the Rackup file:

$ rackup config.ru

Now let's make a HEAD request to the application with curl.

$ curl -I http://localhost:9292
HTTP/1.1 200 OK
Connection: close
Date: Sun, 30 May 2010 21:51:50 GMT
Transfer-Encoding: chunked
Content-Type: text/plain

You can see that we get a valid HTTP response from the server with an empty body. Next, let's add a short message for the response body:

class RailsNutshellApp
  def call(env)
    [200, {'Content-Type' => 'text/plain'}, ['Hello from Rack!']]
  end
end

run RailsNutshellApp.new

The response body contains the message we set:

$ curl http://localhost:9292
Hello from Rack!

As you can see, the concept behind Rack is incredibly simple and easy to use. Next we're going to take a closer look at some of the main principles behind Rack.

Environment

Rack is based on the concept of a request environment. The environment is a hash of CGI-like headers. For details on the exact format and requirements of the Rack environment you can view the complete Rack spec at http://rack.rubyforge.org/doc/files/SPEC.html.

Let's create a simple Rack application that pretty prints the Rack environment as its response body:

require 'pp'

class InspectEnvApp
  def call(env)
    PP.pp(env, response = '')
    [200, {'Content-Type' => 'text/plain'}, [response]]
  end
end

run InspectEnvApp.new

The application pretty prints the Rack env variable into the response string. Then the application returns inspected environment as the response body. Requesting the application with curl yields the following response:

$ curl http://localhost:9292
{"HTTP_HOST"=>"localhost:9292",
 "HTTP_ACCEPT"=>"*/*",
 "SERVER_NAME"=>"localhost",
 "rack.url_scheme"=>"http",
 "REQUEST_PATH"=>"/",
 "HTTP_USER_AGENT"=>
  "curl/7.19.7 (i386-apple-darwin10.2.0) libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3",
 "rack.errors"=>
  #<Rack::Lint::ErrorWrapper:0x101a66c90 @error=#<IO:0x1001b4a80>>,
 "SERVER_PROTOCOL"=>"HTTP/1.1",
 "rack.version"=>[1, 1],
 "rack.run_once"=>false,
 "SERVER_SOFTWARE"=>"Mongrel 1.1.5",
 "PATH_INFO"=>"/",
 "REMOTE_ADDR"=>"127.0.0.1",
 "SCRIPT_NAME"=>"",
 "rack.multithread"=>true,
 "HTTP_VERSION"=>"HTTP/1.1",
 "rack.multiprocess"=>false,
 "REQUEST_URI"=>"/",
 "SERVER_PORT"=>"9292",
 "REQUEST_METHOD"=>"GET",
 "QUERY_STRING"=>"",
 "rack.input"=>
  #<Rack::Lint::InputWrapper:0x101a66d08 @input=#<StringIO:0x101a6bb28>>,
 "GATEWAY_INTERFACE"=>"CGI/1.2"}

In the output you can see the hash of CGI-like headers mentioned earlier. It is the fact that the Rack environment hash conforms to the Rack specification that makes Rack so powerful. Rack application and framework developers can rely on the fact that the environment follows the specification and stop having to worry about different web server APIs.

You can also add your own data to the environment hash, as well as change or delete any of the information. You have to be careful when changing the environment to ensure that your modified environment still conforms to the Rack spec. You never know what other components might be relying on the data in the environment.

Middleware

A Rack middleware is a Rack application that is designed to run in conjunction with another Rack application, which acts as the endpoint. Middlewares are useful for performing granular tasks that can be performed independently from the main application. In fact, Rack itself ships with many useful middlewares for common tasks such as logging requests in the Apache common log format, calculating the Content-Length header based on the size of the response body, setting a default Content-Type header, calculating ETag headers, displaying unhandled exceptions in a pretty error page, and more.

You can think of a Rack middleware as a filter that receives the Rack environment for the request from the previous middleware, if any, does some work with or on the request's environment and then calls the next middleware in the chain. The last Rack application in the chain is the application itself. Any middleware in the chain can return the Rack response itself, thus preventing the rest of the middlewares in the chain from executing. You can think of a middleware as being similar to a controller's the section called “After and Around Filters”.

We've been using the rackup command to start up our application. Rackup actually adds a few middlewares on your behalf in the default Rack environment, which is development. Rackup automatically adds the Rack::ShowExceptions, Rack::CommonLogger, and Rack::Lint middlewares to your application's stack. In the environment named deployment, Rackup only adds the Rack::CommonLogger middleware and in all other named environments Rackup doesn't add any additional middlewares. The Rack::CommonLogger writes a log statement to STDOUT in the Apache common log format for each request:

$ curl -I http://localhost:9292
HTTP/1.1 200 OK
Connection: close
Date: Sun, 30 May 2010 22:21:56 GMT
Transfer-Encoding: chunked
Content-Type: text/plain

The Rack::ShowExceptions middleware renders a nice looking errors page for all unhandled exceptions and Rack::Lint ensures that your Rack application conforms to the Rack spec. Rack::Lint will generate an exception if the response of your application does not meet the Rack spec. To view the error page that Rack::ShowExceptions generates, let's return an empty Content-Type header, which violates the Rack spec and causes Rack::Lint to raise an exception:

class RailsNutshellApp
  def call(env)
    [200, {}, []]
  end
end

run RailsNutshellApp.new

Now when requesting the application we get a nice error page provided by the Rack::ShowExceptions middleware.

Figure 10.1. Error Diagnostic Page Provided By Rack::ShowExceptions

Error Diagnostic Page Provided By Rack::ShowExceptions

It is helpful to look at the sequence diagram for the request in Figure 10.2, “Sequence Diagram of a Request to a Rack Application”. The diagram assumes that Mongrel is the web server handling the request. From the diagram you can see how each middleware in the stack calls the call method on the subsequent middleware. The diagram assumes that none of the middlewares in the stack return the response themselves, which is possible in a real middleware. Each middleware in the chain has its call method executed and executes the next component's call method. This architecture is why Rack middlewares act like controller around filters: code can be executed both before and after the execution of the next component's call method.

Figure 10.2. Sequence Diagram of a Request to a Rack Application

Sequence Diagram of a Request to a Rack Application

In the config.ru Rackup file we can add the Rack::ContentLength middleware to with the use directive. The use directive adds a middleware to the application's middleware stack and the run directive specifies the main Rack application to run.

Let's add the Rack::ContentLength middleware to our application's Rackup file to automatically set a Content-Type header for us. The Rack::ContentType middleware sets the Content-Type to text/html by default, but we're going to configure it to return text/plain by default:

class RailsNutshellApp
  def call(env)
    [200, {}, []]
  end
end

use Rack::ContentType, 'text/plain'
run RailsNutshellApp.new

Now, we can request the application without raising an exception because the Rack::ContentType middleware is doing its job of setting the Content-Type header:

$ curl -I http://localhost:9292
HTTP/1.1 200 OK
Connection: close
Date: Sun, 30 May 2010 22:21:56 GMT
Transfer-Encoding: chunked
Content-Type: text/plain

In addition to all of the middlewares built-in to Rack itself, there is a companion project called Rack Contrib that contains many more useful middlewares that can help speed up your development. Additionally, there is the Rack Cache, which enables a ton of HTTP caching functionality for your Rack application.

Rackup

Rackup files are a simple DSL for specifying which Rack components will be used by the application. The Rackup DSL is implemented by the Rack::Builder class in Rack. You can use any Ruby syntax you want in the Rackup file in addition to the following three methods provided by the DSL.

run(app)

Specifies the actual Rack application to run at the end of the middleware chain.

class MyApp
  def call(env)
    [200, {'Content-Type' => 'text/plain'}, []]
  end
end

run MyApp.new
use(middleware, *args)

Specifies a middleware to add to the application's middleware stack. Middlewares are added in the order they are specified. Any additional arguments are passed along to the initialize method of the middleware.

class MyApp
  def call(env)
    [200, {}, []]
  end
end

use Rack::CommonLogger
use Rack::ContentLength
use Rack::ContentType
use Rack::ETag
run MyApp.new
map(path){ block }

Mounts a Rack application under the specified URI path. An application is mounted under the URI path by calling run within a map block.

class App1
  def call(env)
    [200, {}, ['App 1']]
  end
end

class App2
  def call(env)
    [200, {}, ['App 2']]
  end
end

use Rack::ContentType, 'text/plain'
use Rack::ContentLength

map '/' do
  run App1.new
end

map '/admin' do
  run App2.new
end

Mapping a URI path matches the exact URI path and any path of the URI:

$ curl http://localhost:9292
App 1

$ curl http://localhost:9292/products
App 1

$ curl http://localhost:9292/admin
App 2

$ curl http://localhost:9292/admin/orders
App 2

$ curl http://localhost:9292/administrator
App 1

Using Rack in your Rails Application

Rails has adopted the Rack philosophy throughout the framework. A Rails application is actually a collection of Rack and Rails middleware components that all work together to form the completed whole. However, it is not immediately obvious that Rails utilizes Rack so heavily because Rails' use of Rack is deep within the core infrastructure of the framework. This is because Rack is a low-level library and specification and Rails is a high-level framework. The concepts shown throughout the beginning of this chapter are more to give an understanding of Rack than to provide immediate benefit to your application development. It isn't often that you'll need to think about Rack while developing your application, but it can be a handy tool.

Depending on what your middleware does, there are two different ways to install Rack components into your Rails application. You can either configure your Rack application as part of your application's middleware stack or you can route URI paths directly to the Rack application from you application's routes.

The Rails Middleware Stack

You can view a list of the middleware stack your application has configured by running the rake middleware Rake task from the root of your application:

Example 10.1. Listing the Rails Middleware Stack

$ rake middleware
use ActionDispatch::Static
use Rack::Lock
use ActiveSupport::Cache::Strategy::LocalCache
use Rack::Runtime
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::RemoteIp
use Rack::Sendfile
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::MethodOverride
use ActionDispatch::Head
run RailsNutshell::Application.routes

It isn't important to worry about the details of each middleware listed, but there is one interesting thing to note: the Rack application being run with the run directive at the end of the list of middlewares is the Rails application's routes. This gives some insight into how the request cycle works in your application. The entire Rack middleware stack is executed and then the application's routes are passed the Rack environment. The URI path of the current request is matched to a route in the route set defined in config/routes.rb. Once a route has been matched, the controller action it maps to is executed as a Rack application. You can think of Rails as being "Rack all the way down".

To get some insight into how the Rails request/response cycle works we can first take a look at how the Rails application's route set matches the incoming request URI. Consider the following controller:

class RackController < ApplicationController
  def index
    render :text => 'Rack all the way down'
  end
end

In order for the example to work you'll also need a route defined in config/routes.rb:

get 'rack/index'

To do this you can fire up the Rails console:

app = RailsNutshell::Application.routes
app.class                # => ActionDispatch::Routing::RouteSet

env = { 'REQUEST_METHOD' => 'GET',
        'PATH_INFO' => '/rack/index',
        'rack.input' => StringIO.new }
        
code, headers, body = app.call(env)

code                     # => 200
headers.keys             # => ["ETag", "Content-Type", "Cache-Control"] 
headers['ETag']          # => "\"364cd4b7c6facba9ff60c3fd96f0ffd9\""
headers['Content-Type']  # => "text/html; charset=utf-8"
headers['Cache-Control'] # => "max-age=0, private, must-revalidate"
body.class               # => ActionDispatch::Response
body.body                # => "Rack all the way down"

Now to prove that Rails really is "Rack all the way down", it is even possible to get a Rack application for a controller action from a controller. From the Rails console, you can execute the following:

app = RackController.action(:index)
env = { 'REQUEST_METHOD' => 'GET', 'rack.input' => StringIO.new }
code, headers, body = app.call(env)

code      # => 200
body.body # => "Rack all the way down"

First, we retrieve the Rack application for the controller's index action. Then we assign the env hash with a minimal Rack request environment. Finally we execute the call method, passing in the fake request environment env. This returns a Rack compliant response code, headers and body, which we then inspect on the following lines. It is amazing to see that, even deep within the core of Rails, the simplicity of the Rack specification and implementation shines through.

Creating Custom Middleware

It is most likely that if you're dealing with Rack in your Rails application that you're creating a custom middleware. Adding a middleware into the middleware stack is a nice way to add a bit of functionality that stands completely on its own and that you hopefully never have to look at again after you've finished writing it. Middlwares are out of sight, out of mind, because they are part of the processing pipeline of a Rails application and do not clutter up your models or controllers.

To create a custom middleware you first need to create a new empty Rack middleware application that takes an instance of the application as its first argument, and one or more additional arguments as options. See the definition of the use method above for the proper interface for a middleware component.

As you can see from Example 10.1, “Listing the Rails Middleware Stack”, Rails is using a Rack middleware component called Rack::Runtime. The Rack::Runtime middleware calculates the time the request took to process and places the result in the X-Runtime response header. This is pretty straightforward, but what if your wanted to calculate the average runtime of the responses for the instance of the application that is running? With a middleware stack this is really easy. All we have to do is hook up a new custom middleware before the Rack::Runtime middleware. Create the file lib/average_runtime.rb and place the following code in it:

class AverageRuntime
  @@requests = 0
  @@total_runtime = 0.0
  
  def initialize(app)
    @app = app
  end
  
  def call(env)    
    code, headers, body = @app.call(env)
    
    @@requests += 1
    @@total_runtime += headers['X-Runtime'].to_f
    headers['X-AverageRuntime'] = (@@total_runtime / @@requests).to_s

    [code, headers, body]
  end
end

The middleware uses a couple of class variables to store the number of requests that have been handled and the total runtime of the requests. The middleware calls the next middleware in the stack and receives the response into the code, headers, and body variables. It then increments the class variables containing the total number of requests and the total runtime of the application. Finally, the middleware calculates and sets the value of the X-AverageRuntime header and returns the Rack response.

The AverageRuntime middleware needs to be placed into the middleware stack before the Rack::Runtime middleware so that it can read the value of the X-Runtime header. This is easy to do in a Rails application by adding the following code to the bottom of the applications configuration class located in config/application.rb:

config.middleware.insert_before Rack::Runtime, "AverageRuntime"

We used the string "AverageRuntime" instead of the constant when inserting the middleware because, at the time of the declaration, the lib directory hasn't been added to the application's load path. Using the constant would result in a NameError exception being raised during the loading of the application.

You will now see the AverageRuntime middleware listed in the middleware stack if you run the rake middleware command again. You can also only include the middleware in a particular Rails environment by moving the config.middleware.insert_before statement from config/application.rb into a particular environment's configuration file. Making a few requests to the application shows that it is calculating the average runtime of the application:

$ curl -I http://localhost:3000
HTTP/1.1 200 OK
Connection: close
Date: Sat, 05 Jun 2010 18:27:46 GMT
ETag: "0019716c49d9cfe3d66d3223729c3cdf"
Content-Type: text/html; charset=utf-8
X-Runtime: 0.038098
Content-Length: 0
Cache-Control: max-age=0, private, must-revalidate
X-AverageRuntime: 0.16787325

Installing Middleware

There are several options available when installing a middleware into the middleware stack. When installing middlewares, you can specify the name of the middleware as a class name string or a constant. Any additional arguments passed in *args are passed to the middleware's initialize method after the first argument, which is always the application. The available middleware configuration methods are:

use(middleware, *args)

Adds the middleware to the end of the middleware stack.

insert_before(target, middleware, *args)

Insert middleware into the middleware stack before the target middleware.

insert_after(target, middleware, *args)

Insert middleware into the middleware stack after the target middleware.

swap(target, middleware, *args)

Remove target middleware and replace it with middleware .

You can configure the middleware stack using either the config.middleware settings in your application's config files or through the methods on the Rails.application object. Configuration in a Rails configuraiton file, such as config/application.rb looks like:

config.middleware.use "AverageResponse"

Using the Rails.application object looks like:

Rails.application.middleware.use "AverageResponse"

Routing to a Rack Application

Rails lets you define routes that are handled by a Rack application instead of a Rails controller. This allows you to create lightweight Rack endpoints for those performance critical actions where every millisecond counts. As we saw earlier, the simplest Rack application is simply a Ruby Proc object that receives the Rack environment and returns a response. We could define a Proc object directly in the config/routes.rb file, but that isn't very maintainable. Instead, let's define a simple new Rack application in the lib folder of our application. Create lib/rack_app.rb and place the following code in it:

class RackApp
  def call(env)
    [200, {'Content-Type' => 'text/plain'}, ['Rack Powered!']]
  end
end

Now connect the /rack path to the application in config/routes.rb:

get 'rack' => RackApp.new

Now when requesting the /rack path of the application the tiny Rack application will provide the response:

$ curl http://localhost:3000/rack
Rack Powered!

You can even route to another web framework that is built on top of Rack. Sinatra is a very powerful Ruby based micro-framework. Let's route /sing to a simple Sinatra application. First, make sure you have Sinatra configured in your Gemfile:

gem 'sinatra'

Then define a simple Sinatra application in lib/sinatra_app.rb:

class SinatraApp < Sinatra::Base
  get '/sing' do
    'Singing with Sinatra!'
  end
  
  get '/sing/:song' do
    "Singing #{params[:song].capitalize}!"
  end
end

Now we can hook up a route in config/routes.rb to map /sing and optionally, /sing/:song to the Sinatra application:

get 'sing(/:song)' => SinatraApp

Now the /sing path of the application is being served by the Sinatra application:

$ curl http://localhost:3000/sinatra
Singing with Sinatra!

If we optionally provide a song name in the requested path then it will hit the /sing/:song action in the Sinatra application:

$ curl http://localhost:3000/sing/lovers
Singing Lovers!

As you can see, it is very easy to route URI paths of your application to a Rack application. This is a very powerful concept that allows application developers to easily mix and match different Rack components to suit their needs.

Site last updated on: December 11, 2012 at 10:21:28 PM PST
Cover for Rails 3 in a Nutshell

View 1 comment

  1. hongbo zhang – Posted Nov. 23, 2012

    This seems do not explain it in details.

Add a comment

View 1 comment

  1. Mike Ewing – Posted Nov. 23, 2011

    The simplest example is a Ruby object, such as a what?

Add a comment

View 1 comment

  1. Mike Ewing – Posted Nov. 23, 2011

    "stop having to worry about" is awkward

    "stop worrying about" is better

    Edited on November 23, 2011, 7:08 a.m. PST

Add a comment

View 2 comments

  1. ches – Posted May 13, 2011
    You can think of a middleware as being similar to a controller's the section called “Around Filters”.
    

    This doesn't make sense :-)

  2. Bonnie Cheung – Posted Nov. 26, 2011

    "You can think of a Rack middleware as a filter that receives the Rack environment for the request from the previous middleware, if any, does some work with or on the request's environment and then calls the next middleware in the chain."

    This sentence can be written to be clearer.

Add a comment

View 1 comment

  1. ches – Posted May 13, 2011

    Typo: Rack::Link should be Rack::Lint.

Add a comment

View 1 comment

  1. Shripad – Posted Aug. 27, 2010

    "Let's add the Rack::ContentLength middleware"... Should it not be Rack::ContentType instead?

Add a comment

View 1 comment

  1. Shripad – Posted Aug. 27, 2010

    "It is most likely that if you're dealing with Rack in your Rails application then you're creating a custom middleware."

Add a comment

View 1 comment

  1. Jess – Posted Sept. 1, 2010

    Typo! ... but what if your wanted... your = you

Add a comment

View 2 comments

  1. Shripad – Posted Aug. 27, 2010

    Get the following error:

    rake aborted! uninitialized constant AverageRuntime

  2. Shripad – Posted Aug. 27, 2010

    Had to uncomment and change

    config.autoload_paths += %W(#{config.root}/lib)

    and then,

    config.middleware.insert_before Rack::Runtime, "AverageRuntime"

Add a comment