1988

Advanced Rack

Almost exactly one year ago, I started teaching Metis, and Josh and I taught Sinatra to the students. A few of the students asked about how Sinatra works, and specifically how it’s built. We didn’t go too in-depth in class, but it piqued my curiousity and I started learning a lot more about Rack.

For this post, I’m going to assume that you have a general understanding of HTTP (for example: 2xx status code means success) and Ruby, though you may never have built a Rack app before. OK? Cool.

Let’s dive in to Rack.

What is Rack?

In a sentence, Rack specifies a standard interface for Ruby webservers.

All major Ruby web servers (Puma, WEBrick, Unicorn, etc) understand the Rack protocol, so if your app conforms to the Rack application specification you can use those servers with it for free.

Rack is a great foundation for web frameworks, like Rails and Sinatra. It gives you a Ruby-flavored interface to the HTTP request/response cycle. Since Rails is built on Rack, understanding Rack will help you write better Rails code and tests, including making fake remote services and catching JSON errors.

The Rack specification

Straight from the Rack City Department of Records, where the official Rack docs live:

A Rack application is a Ruby object (not a class) that responds to call. It takes exactly one argument, the environment, and returns an Array of exactly three values: The status, the headers, and the body.

As you can see, there’s very little overhead to making a Rack application. It’s an object that takes in a well-specified object and returns another well-specified object. It’s Just Ruby™.

Let’s build a Rack app and break it down. Before going farther, install the rack gem, which has some helpful utilities that we’ll use:

gem install rack

A minimal Rack app

Here’s a Rack app that responds with the type of request you send it:

# cool_rack_application.rb
require "rack"

class CoolRackApplication
  def call(env)
    http_verb = env["REQUEST_METHOD"]
    status = 200
    headers = {}
    body = ["got #{http_verb} request\n"]

    [status, headers, body]
  end
end

# Run on localhost, port 9292
Rack::Handler::WEBrick.run(CoolRackApplication.new, Port: 9292)

As you can see, it’s Plain Old Ruby: CoolRackApplication doesn’t inherit from anything, and it doesn’t mix in any modules.

Try running that right now with ruby cool_rack_application.rb, then visit http://localhost:9292.

Neat, right?

Let’s go over that code, starting with the env parameter.

The Rack environment

The env variable that gets passed in to call is the Rack environment, and you can read about what’s in it in the Rack docs. It has a lot of information about the request, like what HTTP verb was used, the request path, the host, and more.

We’re grabbing env["REQUEST_METHOD"], which is a string referring to the verb, like "GET" or "POST". Then we respond to requests with a 200 response with body text like “got POST request”. The Rack specification says the body (the third element in the array) must respond to #each, so we wrap it in an array.

Rack::Request

An even easier way to use the Rack environment is to wrap it in Rack::Request, like so:

request = Rack::Request.new(env)

Rack::Request also lets us replace direct env access like env["REQUEST_METHOD"] with easy-to-read methods like request_method. Here are some quick comparisons of direct env access versus Rack::Request:

# env["REQUEST_METHOD"]
request.request_method

# env["rack.request.query_hash"] + env["rack.input"]
request.params

# env["REQUEST_METHOD"] == "GET"
request.get?

Accessing env directly quickly becomes tedious in any Rack project. You should use Rack::Request.

Handlers

Let’s look again at this line:

Rack::Handler::WEBrick.run(CoolRackApplication.new, Port: 9292)

Rack uses handlers to run Rack applications. Each Ruby webserver has its own handler, but I chose the WEBrick handler for this example, because WEBrick is installed by default with Ruby.

If Puma is installed, we can use the Puma runner:

require "rack/handler/puma"

Rack::Handler::Puma.run(CoolRackApplication.new, Port: 9292)

Rack ships with a few built-in handlers, and other Ruby webservers provide their own, like Puma.

Middleware

CoolRackApplication receives the HTTP response and responds directly. This is actually uncommon in the Rack world - more often we want to process the request or response before it hits our final application. The way we do that is with middleware: other Rack applications that come between our final app and the HTTP request. Middleware is Rack’s true strength.

Here’s some middleware that blocks every PATCH request:

class PatchBlockingMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    request = Rack::Request.new(env)

    if request.patch?
      [405, {}, ["PATCH requests not allowed!\n"]]
    else
      @app.call(env)
    end
  end
end

PatchBlockingMiddleware (which, again, is another Rack app) has a call method that acts just like our call method above - it takes in an env and returns a 3-element array. The difference is that it’s initialized with an app, which is the “final” Rack app that it will pass the request on to.

In the PatchBlockingMiddleware above, if the request is a PATCH request, it immediately returns a 405 response with an error message. It doesn’t pass the request on; it halts immediately and gets no farther. Otherwise, it passes control on to the app.

Let’s look at Rack::Builder and how it works to clear it up a little more.

Rack::Builder

In a sentence, Rack::Builder is how you combine middleware and Rack apps. Here’s how you’d combine PatchBlockingMiddleware and CoolRackApplication:

app = Rack::Builder.new do
  use PatchBlockingMiddleware
  run CoolRackApplication.new
end

Rack::Handler::WEBrick.run(app, Port: 9292)

Every incoming request goes through PatchBlockingMiddleware first. If PatchBlockingMiddleware passes it on with @app.call(env), then the request gets passed to CoolRackApplication.new.

Try putting the new Rack::Builder code in cool_rack_application.rb with the classes we’ve built already and running it. Then watch it block requests:

# This PATCH request will be blocked
$ curl -X PATCH localhost:9292
# This GET request will not be blocked
$ curl -X GET localhost:9292

We can stack on as much middleware as we want:

app = Rack::Builder.new do
  use PatchBlockingMiddleware
  use OtherMiddleware
  use Rack::Deflatermaus
  run CoolRackApplication.new
end

# Run the app we built
Rack::Handler::WEBrick.run(app, Port: 9292)

Further Reading

Ruby Learning’s introduction to Rack is excellent.

If you’d like to see a truly silly Rack middleware that I built, check out rack-deflatermaus.