Understanding Rack Apps and Middleware

Facebook
Twitter
LinkedIn

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

  1. Status
  2. Headers
  3. 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.

You might also like:   Rails encrypted credentials on 6.2

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.

You might also like:   The Ruby Unbundled Series: Why You Should Check Out Hotwire Now

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!

Want more posts like this?

What you should do now:

Facebook
Twitter
LinkedIn

Easy Application Deployment to AWS

Focus on development, not on managing infrastructure

Deploying, running and managing your Ruby on Rails app is taking away precious resources? Engine Yard takes the operational overhead out of the equation, so you can keep innovating.

  • Fully-managed Ruby DevOps
  • Easy to use, Git Push deployment
  • Auto scaling, boost performance
  • Private, fully-configured Kubernetes cluster
  • Linear pricing that scales, no surprises
  • Decades of Ruby and AWS experience

14 day trial. No credit card required.

Sign Up for Engine Yard

14 day trial. No credit card required.

Book a Demo