What on Earth is Rack?

Understanding Rack and Middleware. By Yihang Ho
Matt Briney

If you have done some Rails or Sinatra programming, you might have come across enigmatic jargons like Rack and Middleware. So what on Earth are these two things?

Rack

Rack is just an interface or protocol that dictates how a web server, like WEBrick and Thin, communicate with your application. In this context, a web server is a program that listens to HTTP traffic and sends out response to the client. On the other hand, application is the one that generates response for the web server to send to the client.

The Rack interface is incredibly simple. The web server should parse the HTTP request and generate a Ruby Hash.

The application, on the other end of the interface, should respond to call, taking in the Hash produced by the web server, does some processing, and spits out a response, which is a 3-element array.

  • The first element, when parsed as an integer using its to_i method, represents the HTTP response status.
  • The second element is a Hash that represents the response header.
  • The third element is usually an Array of String. In general, it has to respond to each and yield a String when called.

As an example, this is how you implement a simple Rack application:

# config.ru
# Run `rackup config.ru` to start a web server
require 'rack'

app = Proc.new do |env|
  [200, {"Content-Type" => "text/html"}, ["Hello!<br>You requested #{env['PATH_INFO']}"]]
end

run app

Middleware

A middleware is specified by a Ruby class. The constructor of instances of this class takes an object that responds to call (more on this later on); this instance itself should also respond to call. The use method is used to attach middlewares.

Let's build our first middleware that prints logs the incoming request:

require 'rack'

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

  def call(env)
    puts "#{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}"
    @app.call(env)
  end
end

app = Proc.new do |env|
  [200, {"Content-Type" => "text/html"}, ["Hello!"]]
end

use MyLoggerMiddleware
run app

Multiple Middlewares

It is also possible, and often the case, to use multiple middlewares. Simply call use multiple times to attach all the middlewares that will be used:

require 'rack'
require 'middleware_a'
require 'middleware_b'
require 'middleware_c'
require 'app'

use MiddlewareA
use MiddlewareB
use MiddlewareC
run app

As mentioned earlier, when an instance of a middleware is initialized, the constructor receives an object that responds to call. In reality, this object is either the application or an instance of another middleware. When multiple middlewares are attached, only the last middleware will be given the actual application; the rest are given the next middleware. Because of this, the order in which the middlewares are attached is important.

Consider the previous example, the application initialization might look something like this:

middleware_c = new MiddlewareC(app)
middleware_b = new MiddlewareB(middleware_c)
middleware_a = new MiddlewareA(middleware_b)

# When a request comes in:
middleware_a.call(env)

As a result, middlewares "cascade": each middleware can intercept, pre-process, post-process, or do nothing about each request:

Example

In this example, the application will generate an HTML paragraph, and we will implement middlewares responsible of the following:

  1. Returns 404 for unsupported path - HTTPStatusMiddleware.
  2. Wrap the response in a HTML boilerplate - HTMLMiddleware.
  3. Add a footer to the response - FooterMiddleware.
require 'rack'

class HTTPStatusMiddleware
  def initialize(app)
    # Here, app is an instance of HTMLMiddleware
    @app = app
  end

  def call(env)
    if env["PATH_INFO"] =~ /\A\/posts/
      @app.call(env)
    else
      [404, {"Content-Type" => "text/html"}, ["Not Found"]]
    end
  end
end

class HTMLMiddleware
  def initialize(app)
    # Here, app is an instance of FooterMiddleware
    @app = app
  end

  def call(env)
    res = @app.call(env)
    content = res.last
    content.unshift <<-HEREDOC
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>My Blog</title>
      </head>
      <body>
    HEREDOC
    content.push <<-HEREDOC
      </body>
    </html>
    HEREDOC
    res
  end
end

class FooterMiddleware
  def initialize(app)
    # Here, app is the Proc we define below
    @app = app
  end

  def call(env)
    res = @app.call(env)
    res[2].push("<footer>Copyright Yihang Ho 2014</footer>")
    res
  end
end

app = Proc.new do |env|
  [200, {"Content-Type" => "text/html"}, ["<p>Hello, this is my blog, powered (proudly) by Rack.<p>"]]
end

use HTTPStatusMiddleware
use HTMLMiddleware
use FooterMiddleware
run app

Take note that the order in which the middlewares are attached is important in this example, especially the relative order of HTMLMiddleware and FooterMiddleware. If these two middlewares are swapped, the footer tag will appear after the closing HTML tag!