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.newBelieve 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.newThe 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.newThe 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.newNow when requesting the application we get a nice error
page provided by the Rack::ShowExceptions middleware.
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.
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.newNow, 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.newuse(middleware,*args)Specifies a
middlewareto add to the application's middleware stack. Middlewares are added in the order they are specified. Any additional arguments are passed along to theinitializemethod 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.newmap(path){block}Mounts a Rack application under the specified URI
path. An application is mounted under the URI path by callingrunwithin amapblock.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 endMapping 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
endIn 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
endThe 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
middlewareto the end of the middleware stack.insert_before(target,middleware,*args)Insert
middlewareinto the middleware stack before thetargetmiddleware.insert_after(target,middleware,*args)Insert
middlewareinto the middleware stack after thetargetmiddleware.swap(target,middleware,*args)Remove
targetmiddleware and replace it withmiddleware.
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
endNow 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
endNow 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.







View 1 comment




This seems do not explain it in details.
Add a comment