This post originally appeared on Engine Yard.
For many of us web developers, we work on the highest levels of abstraction when we program. 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 or middlewares work, so I spent a little time finding out. In this post, I’ll share what I learned.
It’s possible to quickly build simple web applications using just Rack.
To get started, all you need is 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 like
Rack::Handler::WEBrick, or put it into a
config.ru file and run it from the command line with
Ok, cool. So what does Rack actually do?
How Rack Works
Rack is really just a way for a developer to create a server application while avoiding the boilerplate code that would be required to do so using
Net::HTTP. If you’ve written some code that meets the Rack specifications, you can load it up in a Ruby server like
Thin, and you’re 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
Takes an application (the object that responds to
call) as an argument. The following code from the Rack website demonstrates how this looks:
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:
1 2 3
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
Your Rack server object takes 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_PORT: The server’s address and port.
rack.version: The rack version in use.
rack.url_scheme: is it
rack.input: an IO-like object that contains the raw HTTP POST data.
rack.errors: an object that response to
rack.session: A key value store for storing request session data.
rack.logger: An object that can log interfaces. It should implement
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,
logger return the values from the keys described above. It also lets you check out things like the
scheme, or whether you’re using
ssl?. For a complete listing of methods, I would suggest digging through the source.
When your Rack server object returns a response, it must contain three parts: the status, headers, and body. As there was for the request, there is a Rack::Response object that gives you convenience methods like
finish, and more. Alternately, you can just return an array containing the three components.
An HTTP status, like 200 or 404.
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-Length if it’s appropriate for your response.
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 actually 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 a 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 inbound requests and outbound responses. Why would you want to do that? There are tons of middleware components available for Rack that take the guesswork out of problems like enabling caching, authentication, trapping spam, and many other problems.
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.
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
gem "warden" and install it with
Now add it to your
1 2 3 4 5 6 7 8 9 10 11 12
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 fleshed-out example of integrating Warden, check out this gist.
By the way, 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 middlewares and app(s) in one big application. For example:
1 2 3 4 5 6 7 8 9 10 11 12
Rack Basic Auth
One really useful piece of middleware is
Rack::Auth::Basic, which you can use to protect any Rack app with HTTP basic authentication. It is really lightweight and comes in handy 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:
1 2 3
That was easy!
Using Middleware in Rails
Now, so what? Rack is pretty cool, and we know that Rails is built on it. But just because we understand what it is, doesn’t make it actually useful in working with a production app.
How Rails Uses Rack
Did you ever notice 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:
1 2 3 4
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/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
rails/all, loads up the environment (test, development, production, etc.), and defines a namespaced version of our application. It looks something like this:
1 2 3 4 5
So I guess that means that
Rails::Application must be a Rack app? Sure enough! If we check out the source code, it responds to
So what middleware is it using? Well, I see that it’s autoloading
rails/application/default_middleware_stack. Checking that out, it looks like it’s defined in
ActionDispatch. Where does
ActionDispatch come from?
Action Pack is Rails’s framework for handling web requests and responses. Action Pack home to quite a few of the niceties you find in Rails, such as routing, the the abstract controllers that you inherit from, and view rendering.
The most relevant part of AP 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 Action Dispatch middleware components, you’ll notice they’re all following the Rack specification: they all respond to
call, taking in an
app and returning
body. Many of them also make use of
For me, 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
https://api.myawesomeapp.com, and the client-side app lives at
You’re going to run into a problem pretty quick: you can’t access resources at
api.myawesomeapp.com from your JS 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
As with so 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:
1 2 3 4 5 6 7 8 9 10 11 12 13
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).
Now if you reboot the server, you should be good to go! Your client-side app can access
api.myawesomeapp.com without running into same-origin policy JS errors.
If you want to learn more about how HTTP requests are routed through Rack in Rails, I’d suggest taking a look at this tour of the Rails source code that deals with handling requests.
In this post, we’ve take an in-depth at the internals of Rack, and by extension, the request/response cycle for several Ruby web frameworks, including Ruby on Rails.
Hopefully, understanding what’s going on when a request hits your server and your application sends back a response helps make things feel a little less magical. Because I don’t know about you, but when things go wrong, I have a lot harder time troubleshooting 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.
If I’ve done my job, reading this article will enable you to do the same thing.
P.S. 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! Leave us a comment!