Understanding Rack Apps and Middleware

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.

What’s Rack?

Did you know that Rails is a Rack app? Sinatra too. What is Rack? I’m glad you asked. Rack is a Ruby package that provides an easy-to-use interface to the Ruby Net::HTTP library.

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 rackup config.ru.

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 WEBrick, Mongrel, or 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 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

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_NAME and SERVER_PORT: The server’s address and port.
  • rack.version: The rack version in use.
  • rack.url_scheme: is it http or https?
  • rack.input: an IO-like object that contains the raw HTTP POST data.
  • rack.errors: an object that response to puts, write, and flush.
  • rack.session: A key value store for storing request session data.
  • rack.logger: An object that can log interfaces. It should implement info, debug, warn, error, and fatal 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 whether 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: the status, headers, and body. As there was for the request, there is a Rack::Response object that gives you convenience methods like write, set_cookie, finish, and more. Alternately, you can just 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 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.

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 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:

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 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:

use Rack::Auth::Basic, "Restricted Area" do |username, password|
  [username, password] == ['admin', 'abc123']
end

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:

# This file is used by Rack-based servers to start the application.

require ::File.expand_path('../config/environment', __FILE__)
run Rails.application

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

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 call!

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? ActionPack.

Action Dispatch

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 status, headers, and body. Many of them also make use of Rack::Request and Rack::Response objects.

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

Imagine you are 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 quick: you can’t access resources at api.example.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 Gemfile:

gem "rack-cors", require: "rack/cors"

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:

module MyApp
  class Application < Rails::Application
    config.middleware.insert_before 0, "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).

Now 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 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.

Conclusion

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!

About Ben Lewis

Ben is a Colorado native and a third-generation programmer. In his past lives, he was an organic farm worker, gardener, and elementary school music teacher. He started programming at Turing school, where he focused on Test-Driven Development, Single Page Apps, and Service-Oriented Architecture. He lives in Boulder with his daughter Lumin, and enjoys cooking, hiking, yoga, and making music. Ben tweets as @fluxusfrequency and works for Twitter.