Rack, The Underbelly Of Every Great Ruby Web App

Rack is used for practically every framework that runs on Ruby yet many Rails developers don't really understand what it is or how it works. Understanding rack can make these frameworks feel less daunting. It's surprisingly easy to understand when you take a look at it.

Rack is just a stack of callable objects

Rack's interface and principals are actually very simple, it's just that we typically never need to think about them when working with Rails, Sinatra, Hanami or other frameworks built on top of Rack.

So what does Rack actually do?


It is an interface between your web server and your application code! Rack listens to the web server through a handler and then calls your application with one argument, the Rack Environment. To comply with the interface your application must return an array with 3 items, the status, headers, and the body. The status must be an integer which matches a valid HTTP status code. The headers should be a hash of header key value pairs. Finally the body must be an object that responds to each, typically an array of strings.

Hello, World?


Let's build our first Rack app. Rack has built in handlers for various web servers so we'll use one of those.

# hello_world.rb
require 'rack'

class HelloWorld
  def call(env)
    [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]]
  end
end
Rack::Handler::WEBrick.run HelloWorld.new

Now start it up and visit localhost:8080

ruby hello_world.rb

Let's Rackilize Our Hello, World! App.


The application above is called through the ruby executable, however Rack provides an executable named rackup to start our rack applications for us. This executable expects a file at the root of your application called config.ru (r[ack]u[p]) which is used to configure the rack middleware stack.

require 'thin'

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

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

  def call(env)
    status, headers, body = @app.call(env)
    body << "Goodbye, World!\n"
    [status, headers, body]
  end
end

use GoodbyeWorld
run HelloWorld.new

Now we've converted our Rack script to a Rack App configuration. We've also made a couple of changes. We've required thin web server which will automatically switch the handler out. We've also added our first middleware to the stack, GoodbyeWorld. And finally we can use the rack DSL methods, use and run.

So what does this middleware do? In this case we're running the app first and then we're adding our "Goodbye, World!" to the response body.

Goodbye, World!


We can do other, not-so-contrived, things with middleware too. Let's add a simple profiler.

require 'thin'

class HelloWorld
  def call(env)
    [200, {"Content-Type" => "text/plain"}, ["Hello, World!\n"]]
  end
end
class GoodbyeWorld
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)
    body << "Goodbye, World!\n"
    [status, headers, body]
  end
end

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

  def call(env)
    start_time = current_time
    status, headers, body = @app.call(env)
    end_time = current_time
    body << "Took #{end_time - start_time} seconds to complete"
    [status, headers, body]
  end

  def current_time
    Time.now.to_f * 1000
  end
end

use Benchmarker
use GoodbyeWorld
run HelloWorld.new

Modifying the Environment


So far we've only shown how to wrap the application, let's change that so we're just modifying environment instead. It's pretty simple, rather than calling the app and modifying the response, we modify the env and then call the app.

require 'thin'

class HelloWorld
  def call(env)
    if env['flip'] == 'heads'
      [200, {"Content-Type" => "text/plain"}, ["Hello, World!\n"]]
    else
      [401, {"Content-Type" => "text/plain"}, ["Naughty naughty\n"]]
    end
  end
end

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

  def call(env)
    env['flip'] = %w[heads tails].sample
    @app.call(env)
  end
end

use CoinFlip
run HelloWorld.new

Halting execution


If any of the middlewares return a response the rest of the stack will not be processed. This is used by some rack middlewares like rack-attack which prevent attackers from making malicious requests to our app. 

Let's modify the logic make our requests win randomly.

require 'thin'

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

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

  def call(env)
    if %w[heads tails].sample == 'heads'
      @app.call(env)
    else
      [200, {"Content-Type" => "text/plain"}, ["Winner winner chicken dinner!\n"]]
    end
  end
end

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

  def call(env)
    status, headers, body = @app.call(env)
    body << "Goodbye, World!\n"
    [status, headers, body]
  end
end

use CoinFlip
use GoodbyeWorld
run HelloWorld.new
Like 2 likes
Joe Woodward
I'm Joe Woodward, a Ruby on Rails fanatic working with OOZOU in Bangkok, Thailand. I love Web Development, Software Design, Hardware Hacking.
Share:

Join the conversation

This will be shown public
All comments are moderated

Get our stories delivered

From us to your inbox weekly.