Video

Want to see the full-length video right now for free?

Sign In with GitHub for Free Access

Notes

What is Rack

Rack is the underlying technology behind nearly all of the web frameworks in the Ruby world. "Rack" is actually a few different things:

  • An architecture - Rack defines a very simple interface, and any code that conforms to this interface can be used in a Rack application. This makes it very easy to build small, focused, and reusable bits of code and then use Rack to compose these bits into a larger application.
  • A Ruby gem - Rack is is distributed as a Ruby gem that provides the glue code needed to compose our code.

The Rack Interface

Rack defines a very simple interface. Rack compliant code must have the following three characteristics:

  1. It must respond to call
  2. The call method must accept a single argument - This argument is typically called env or environment, and it bundles all of the data about the request.
  3. The call method must return an array of three elements These elements are, in order, status for the HTTP status code, headers, and body for the actual content of the response.

A nice side effect of the call interface is that procs and lambdas can be used as Rack objects.

Rack Hello World

The following code sample shows a bare-bones Rack-compliant application that simply returns the text "Hello World".

require "rack"
require "thin"

class HelloWorld
  def call(env)
    [ 200, { "Content-Type" => "text/plain" }, ["Hello World"] ]
  end
end

Rack::Handler::Thin.run HelloWorld.new

In the sample we implement a class with a single instance method, call, that takes in the env and always returns a 200 (HTTP "OK" status), a "Content-Type" header, and the body of "Hello World".

Note that Rack expects the body portion of the response array to respond to each, so bare strings need to be wrapped in an array. More commonly, the body object will be some other type of IO object, rather than a bare string, so this wrapping is not needed in those cases.

Lastly, we can see that we've required thin, a Ruby web server, and by doing so we can use the Rack compliant handler provided by Thin. Thanks to the simple nature of the Rack interface, nearly all Ruby web servers have implemented a Rack handler and we can largely swap them out, without needing to change our application code. Hooray for useful abstractions!

Exploring the env Object

In our initial sample we simply returned the string "Hello World", but we can actually replace that body with the env object to get a peek inside:

  class HelloWorld
    def call(env)
-     [ 200, { "Content-Type" => "text/plain" }, ["Hello World"] ]
+     [ 200, { "Content-Type" => "text/plain" }, env ]
    end
  end

With that we can see that the env object is a hash containing all of the data about our request and response, including things like the user agent string, cookies, the requested path, etc. All of the values are simply strings, rather than more structured objects like params hash in rails, but everything is included and available for us to inspect and use in composing our response.

For a deeper dive, check out the full specification of the Rack env object.

Using a lambda

Just for fun, and because we can, let's update the hello world sample to use a lambda, rather than a class:

require "rack"
require "thin"

app = -> (env) do
  [ 200, { "Content-Type" => "text/plain" }, env ]
end

Rack::Handler::Thin.run app

Middleware

Middleware are the building blocks of larger applications built using the Rack pattern. Each middleware is a Rack compatible application, and our final application is built by composing together, or nesting these middleware.

Example Logging Middleware

Unlike base Rack apps, middleware must be classes as they need to have an initializer which will be passed the next app in the chain. For our first middleware example, we'll introduce a middleware that logs the amount of time the request took and adds that to the response.

To begin, we'll update our core Rack app to sleep for 3 seconds to give us something worth logging, and then we'll build our middleware:

require "rack"
require "thin"

app = -> (env) do
  sleep 3
  [ 200, { "Content-Type" => "text/plain" }, ["Hello World\n"] ]
end

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

  def call(env)
    before = Time.now.to_i
    status, headers, body = @app.call(env)
    after = Time.now.to_i
    log_message = "App took #{after - before} seconds."

    [status, headers, body << log_message]
  end
end

Rack::Handler::Thin.run LoggingMiddleware.new(app)

Here we can see that the "app" we hand off to the Rack thin handler is our base app, wrapped in our logging middleware. In the call method of the middleware we grab a timestamp, then forward on the request to the next "app" in the chain. Our middleware only knows about the very next app, but there could be multiple layers of middleware before our final base app.

On the line where we call the next app, we destructure the array returned by the app into the status, headers, and body. This allows us to modify or add to these values as needed.

In the logging middleware's case, we include the status and headers in our return array, but we append the logging time string to the body, thus including it in the displayed page.

Example Middleware Usage

Thanks to the simplicity and flexibility of the Rack interface, middleware can be used for a large array of different functions. One interesting example of a Rack middleware is rack-honeypot which works to detect and trap spam and malicious bots interacting with your application. It does this appending a hidden field to all responses which only bots are likely to fill in, then parsing all requests and canceling any that have the hidden field filled in.

Rack::Builder

While the Rack pattern and using middleware is great for composing our applications together, it can get to be a bit verbose. Rack::Builder is a pattern that allows us to more succinctly define our application and the middleware stack surrounding it.

We can define this in a config.ru file which is a Rack specific (.ru stands for "rackup") file, which we can then run directly through the rack gem's executable:

require "thin"

app = -> (env) do
  sleep 3
  [ 200, { "Content-Type" => "text/plain" }, ["Hello World\n"] ]
end

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

  def call(env)
    before = Time.now.to_i
    status, headers, body = @app.call(env)
    after = Time.now.to_i
    log_message = "App took #{after - before} seconds."

    [status, headers, body << log_message]
  end
end

use LoggingMiddleware
run app

The primary changes are no longer needing to require rack explicitly, and using the use and run methods to build our application.

The Middleware Stack

The above Rack::Builder methods can be used more generally to build the "middleware stack" that we wrap around our application. The middleware are listed in a specific order and will be run in that order, with a given request propagating down the stack, eventually reaching our app, then the resulting response object propagating back up through each middleware.

# sample config.ru defining a middleware stack

use FirstMiddleware
use SecondMiddleware
use LoggingMiddleware

run OurApp.new

Running via rackup

Once we have a config.ru to structure our middleware stack, we can run the resulting Rack application using the bare rackup command, provided by the rack gem. By running through rackup, rack will automatically be required and the DSL of the use and run methods will be exposed.

What Can We Do With Middleware?

Middleware are perfect for non-app specific logic. Things like setting caching headers, logging, parsing the request object, etc. all are great use cases for Rack middleware. For example, in Rails, cookie parsing, sessions, and param parsing are all handled by Middleware.

Conclusion

Rack provides a clean and powerful abstraction allowing us to separate the core logic of our application from peripheral concerns like parsing HTTP headers. It is the common interface between nearly all Ruby web applications and the web servers we use to run them. Hopefully this episode has given you the context and inspiration to dive in and get to know Rack just a little better. If you're interested in learning more, be sure to check out Joël's in depth overview of Rack.