Many web developers work on the highest levels of abstraction when we program. And sometimes it’s easy to take things for granted. Especially when we’re using Rails.
Have you ever dug into the internals of how the request/response cycle works in Rails? I recently realized that I knew almost nothing about how Rack apps or middleware works—so I spent a little time finding out. Below are my findings.
What’s Rack?
Did you know that Rails is a Rack app? Sinatra too. What is Rack? Well, I’m glad you asked. Rack is a Ruby package that provides an easy-to-use interface between the web servers and the web frameworks.
It’s possible to quickly build simple web applications using just Rack.
To get started, you need an object that responds to a call method, taking in an environment hash and returning an array with the HTTP response code
, headers
, and response body
. Once you’ve written the server code, all you have to do is boot it up with a Ruby server such as Rack::Handler::WEBrick
, or put it into a config.ru
file and run it from the command line with rackup config.ru
.
Ok, cool. But what does Rack actually do?
Scale performance. Not price. Try Engine Yard today and enjoy our great support and huge scaling potential for 14 days.
Deploy your app for free with Engine Yard.
How Rack Works
Rack is really just a way for a developer to create a server application while avoiding the boilerplate code to support the different Ruby web servers. If you’ve written some code that meets the Rack specifications, you can load it up in a Ruby server like WEBrick
, Mongrel
, or Thin
—and you’ll be ready to accept requests and respond to them.
There are a few methods you should know about that are provided for you. You can call these directly from within your config.ru
file.
run
Takes an application—the object that responds to call—as an argument. The following code from the Rack website demonstrates how this looks:
run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack'd']] }
map
Takes a string specifying the path to be handled and a block containing the Rack application code to be run when a request with that path is received.
Here’s an example:
map '/posts' do
run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['first_post', 'second_post', 'third_post']] }
end
use
This tells Rack to use certain middleware.
So what else do you need to know? Let’s take a closer look at the environment hash and the response array.
The Environment Hash
Rack server objects take in an environment hash. What’s contained in that hash? Here are a few of the more interesting parts:
REQUEST_METHOD
: The HTTP verb of the request. This is required.PATH_INFO
: The request URL path, relative to the root of the application.QUERY_STRING
: Anything that followed?
in the request URL string.SERVER_NAME
andSERVER_PORT
: The server’s address and port.rack.version
: The rack version in use.rack.url_scheme
: Is ithttp
orhttps
?rack.input
: An IO-like object that contains the raw HTTP POST data.rack.errors
: An object that response toputs
,write
, andflush
.rack.session
: A key value store for storing request session data.rack.logger
: An object that can log interfaces. It should implementinfo
,debug
,warn
,error
, andfatal
methods.
A lot of frameworks built on Rack wrap the env
hash in a Rack::Request
object. This object provides a lot of convenience methods. For example, request_method
, query_string
, session
, and logger
return the values from the keys described above. It also lets you check out things like the params
, HTTP scheme
, or if you’re using ssl.
For a complete listing of methods, I would suggest digging through the source.
The Response
When your Rack server object returns a response, it must contain three parts:
- Status
- Headers
- Body
Much like the request, there’s a Rack::Response object that gives you convenience methods like write
, set_cookie
, and finish
. Alternately, you can return an array containing the three components.
Status
An HTTP status, like 200 or 404.
Headers
Something that responds to each and yields key-value pairs. The keys have to be strings and conform to the RFC7230 token specification. Here’s where you can set Content-Type
and Content-Length
if it’s appropriate for your response.
Body
The body is the data that the server sends back to the requester. It has to respond to each and yield string values.
All Racked Up!
Now that we’ve created a Rack app, how can we customize it to make it useful? The first step is to consider adding some middleware.
What is Middleware?
One of the things that makes Rack so great is how easy it is to add chain middleware components between the webserver and the app to customize the way your request/response behaves.
But what is a middleware component?
A middleware component sits between the client and the server, processing both inbound and outbound responses. Why would you want to do that? There are tons of middleware components available for Rack that remove the guesswork from problems like enabling caching, authentication, and trapping spam.
Using Middleware in a Rack App
To add middleware to a Rack application, all you have to do is tell Rack to use it. You can use multiple middleware components and they will change the request or response before passing it on to the next component. This series of components is called the middleware stack.
Warden
We’re going to take a look at how you would add Warden to a project. Warden has to come after some kind of session middleware in the stack, so we’ll use Rack::Session::Cookie
as well.
First, add it to your project Gemfile with gem ‘warden’ and install it with bundle install
.
Now add it to your config.ru file:
require 'warden'
use Rack::Session::Cookie, secret: 'MY_SECRET'
failure_app = Proc.new { |env| ['401', {'Content-Type' => 'text/html'}, ['UNAUTHORIZED']] }
use Warden::Manager do |manager|
manager.default_strategies :password, :basic
manager.failure_app = failure_app
end
run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack'd']] }
Finally, run the server with rackup
. It will find config.ru
and boot up on port 9292.
Note that there is more setup involved in getting Warden to actually do authentication with your app. This is just an example of how to get it loaded into the middleware stack. To see a more robust example of integrating Warden, check out this gist.
There’s another way to define the middleware stack. Instead of calling use
directly in config.ru
, you can use Rack::Builder
to wrap several middleware and app(s) in one big application.
For example:
failure_app = Proc.new { |env| ['401', {'Content-Type' => 'text/html'}, ['UNAUTHORIZED']] }
app = Rack::Builder.new do
use Rack::Session::Cookie, secret: 'MY_SECRET'
use Warden::Manager do |manager|
manager.default_strategies :password, :basic
manager.failure_app = failure_app
end
end
run app
Rack Basic Auth
One useful piece of middleware is Rack::Auth::Basic
, which can be used to protect any Rack app with HTTP basic authentication. It’s really lightweight and is helpful for protecting little bits of an application. For example, Ryan Bates uses it to protect a Resque server in a Rails app in this episode of Railscasts.
Here’s how to set it up:
use Rack::Auth::Basic, 'Restricted Area' do |username, password|
[username, password] == ['admin', 'abc123']
end
That was easy!
Using Middleware in Rails
So what? Rack is pretty cool. And we know that Rails is built on it. But just because we understand what it is, it doesn’t make it actually useful in working with a production app.
How Rails Uses Rack
Have you ever noticed that there’s a config.ru
file in the root of every generated Rails project? Have you ever taken a look inside? Here’s what it contains:
# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment', __FILE__)
run Rails.application
This is pretty simple. It just loads up the config/environment
file, then boots up Rails.application
.
Wait, what’s that? Taking a look in config/environment
, we can see that it’s defined in config/application.rb
. config/environment
is just calling initialize!
on it.
So what’s in config/application.rb
? If we take a look, we see that it loads in the bundled gems from config/boot.rb
, requires rails/all
, loads up the environment (test, development, production, etc.), and defines a namespaced version of our application.
It looks something like this:
module MyApplication
class Application < Rails::Application
...
end
end
This must mean that Rails::Application
is a Rack app. Sure enough, if we check out the source code, it responds to call
!
But what middleware is it using? If we look closely, we see it’s autoloading rails/application/default_middleware_stack
—and checking that out, it looks like it’s defined in ActionDispatch
.
Where does ActionDispatch
come from? ActionPack
.
Action Dispatch
ActionPack is Rails’s framework for handling web requests and responses. ActionPack is home to quite a few of the niceties you find in Rails, such as routing, the abstract controllers that you inherit from, and view rendering.
The most relevant part of ActionPack for our discussion here is Action Dispatch. It provides several middleware components that deal with ssl, cookies, debugging, static files, and much more.
If you go take a look at each of the ActionDispatch middleware components, you’ll notice they’re all following the Rack specification: They all respond to call
, taking in an app and returning status
, headers
, and body
. Many of them also make use of Rack::Request
and Rack::Response
objects.
Reading through the code in these components took a lot of the mystery out of what’s going on behind the scenes when making requests to a Rails app. When I realized that it’s just a bunch of Ruby objects that follow the Rack specification—passing the request and response to each other—it made this whole section of Rails a lot less mysterious.
Now that we understand a little bit of what’s happening under the hood, let’s take a look at how to actually include some custom middleware in a Rails app.
Adding Your Own Middleware
Imagine you’re hosting an application on Engine Yard. You have a Rails API running on one server, and a client-side JavaScript app running on another. The API has a url of https://api.example.com
, and the client-side app lives at https://app.example.com
.
You’re going to run into a problem pretty quickly: You can’t access resources at api.example.com
from your JavaScript app, because of the same-origin policy. As you may know, the solution to this problem is to enable cross-origin resource sharing (CORS). There are many ways to enable CORS on your server—but one of the easiest is to use the Rack::Cors middleware gem.
Begin by requiring it in the Gemfile:
gem 'rack-cors', require: 'rack/cors'
As with many things, Rails provides a very easy way to get middleware loaded. Although we certainly could add it to a Rack::Builder
block in config.ru
—as we did above—the Rails convention is to place it in config/application.rb
, using the following syntax:
module MyApp
class Application < Rails::Application
config.middleware.insert_before , 'Rack::Cors' do
allow do
origins '*'
resource '*',
:headers => :any,
:expose => ['X-User-Authentication-Token', 'X-User-Id'],
:methods => [:get, :post, :options, :patch, :delete]
end
end
end
end
Note that we’re using insert_before here to ensure that Rack::Cors
comes before the rest of the middleware included in the stack by ActionPack (and any other middleware you might be using).
If you reboot the server, you should be good to go! Your client-side app can access api.example.com
without running into same-origin policy JavaScript errors.
Scale performance. Not price. Try Engine Yard today and enjoy our great support and huge scaling potential for 14 days.
Deploy your app for free with Engine Yard.
Conclusion
In this post, we’ve take an in-depth look at the internals of Rack and, by extension, the request/response cycle for several Ruby web frameworks, including Ruby on Rails.
I hope that understanding what’s going on when a request hits your server and your application sends back a response helps make things clear. I don’t know about you, but when things go wrong, it’s much harder to troubleshoot when there’s magic involved than when I understand what’s going on. In that case, I can say, “Oh, it’s just a Rack response” and get down to fixing the bug. And if I’ve done my job, reading this article will enable you to do the same thing.
Do you know of any use cases where a simple Rack app was enough to meet your business needs? What other ways do you integrate Rack apps in your bigger applications? We want to hear your battle stories!