Codenoble

Creating quality websites and web applications

Understanding Rack Middleware

Adam Crownoble

Rack middleware is one of those things that's actually very simple. We'll see how we can implement the basic concept in just a few lines of code. But because of sparse documentation and an unclear API, getting started with Rack middleware is a confusing endeavor for anyone new to the concept.

But fear not. We're going to boil all the core concepts of the Rack middleware stack into an easy to run and understand code example. Along the way, we'll get some insight into what's going on under the hood.

In this article we'll mostly be rolling our own Rack-like code. It's not exactly like Rack, of course. It's meant to have just enough there to be able to grasp the core concepts. Any of the code below can be run form the command line. Just put it together in a file and run ruby ./understanding-middleware.rb

Getting Started

Let's jump right into the deep end. This is actually the most complicated part of what we'll be going through. Everything else is basic, but it has to adhere to the unspoken rules of this code. So let's take a look.

def run_the_stack(middleware, app, env)
  prev_app = app

  middleware.reverse.each do |part|
    part = part.new(prev_app)
    prev_app = part
  end

  prev_app.call(env)
end

It helps to know that we're going to call this like puts run_the_stack [Middleware1, Middleware2], App.new, user: 'Dana'. The key thing to realize is that it starts at the bottom of the stack with the application itself and then goes up the stack backwards through the middleware array, initializing each middleware with the next thing down the stack. That's what makes this a chain of things that will be called each part at a time, because each part will call the next part. Usually anyway. More on that later.

Making An App

So let's add to the code above and write an app to use with our new run_the_stack method.

class Talk
  def call(env)
    "Can I talk to #{env[:name]}?"
  end
end

run_the_stack [], Talk.new, name: 'Dana' #=> "Can I talk to Dana?"

Pretty simple, huh? Here are the rules of writing an app.

  • The app must respond to #call
  • The #call method must take an environment hash
  • The #call method must return a string

The rules of actual Rack are similar but you'll have to return a status, headers and a body instead of just a string. We'll get into that later. For more details on Rack apps see one of my previous articles.

Modifying the Response

There are two types of middleware. Those that modify the request or response, and those that short circuit the stack and return their own response without ever calling anything further down the stack. Let's start with some middleware that modifies the response.

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

  def call(env)
    @app.call(env).upcase
  end
end

The rules for middleware are:

  • Must be able to be initialized with an argument for next app down the stack
  • An instance must respond to #call
  • #call must take an environment hash
  • #call must return a string

You can see that all we're really doing is upper casing the response from the app. Pretty simple. Call it with run_the_stack [Shout], Talk.new, name: 'Dana' and you'll get "CAN I TALK TO DANA?". This sort of middleware is useful if you want to add something like tracking code to each of your pages or maybe automatically censor certain words.

We've just successfully implemented our own middleware stack! It's important to note that you can also modify the env hash however you want, in addition to being able to modify the response. Let's see what else we can do with middleware.

Hijacking The Response

The other type of middleware is the type that returns a response directly without calling the middleware or the app below it. It's useful for triggering errors or redirects. Let's see an example.

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

  def call(env)
    "There is no #{env[:name]}. Only Zuul!"
  end
end

Here you can see we're never executing @app.call so we won't hit next middleware down the stack or the app. Run this code with puts run_the_stack [Zuul, Shout], Talk.new, name: 'Dana' and you'll get "There is no Dana. Only Zuul!". Notice the output isn't capitalized because our Shout middleware never got called. However, if we switch the middleware order puts run_the_stack [Shout, Zuul], Talk.new, name: 'Dana' we'll get "THERE IS NO DANA. ONLY ZUUL!".

The pop culture reference isn't just coincidence. Middleware really is the demon possession of the software world. They sit on top of your brain/app and mess with what it sees and how it responds. Just remember to possess your code responsibly ;)

If you'd like to see the code all together in one place I made a Gist.

Making It A Real Rack App

Our little kludgey imitation of a Rack application isn't too far off from the real thing. So let's make a few changes so we can run it with a Rack server.

class Talk
  def call(env)
    [200, {'Content-Type' => 'text/plain'}, ["Can I talk to #{env['QUERY_STRING']}!"]]
  end
end

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

  def call(env)
    response = @app.call(env)
    [response[0], response[1], response[2].map(&:upcase)]
  end
end

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

  def call(env)
    [200, {'Content-Type' => 'text/plain'}, ["There is no #{env['QUERY_STRING']}. Only Zuul!"]]
  end
end

use Shout
use Zuul

run Talk.new

Obviously there are a few differences:

  • Our ugly run_the_stack method is gone and in it's place we're using the much prettier use and run methods.
  • You won't see us calling anything with our own env hash here because that's handled by the server based on what your browser request contains.
  • Instead of returning a string we're returning an array containing the status code, headers and body in that order. The body must respond to #each, the easiest way to do that is to just stick it in an array.

Throw the code above in a config.ru file, run rackup and open http://localhost:9292?Dana in your browser and you should see THERE IS NO DANA. ONLY ZUUL!. Or, if you like, you can put the Shout and Zuul middleware in your the config.ru of your own Rails project and see them in action.

So, that's about it. Hopefully it's enough to get you interesting in playing around with your own Rack middleware. I've made a couple middleware projects myself if you'd like to dig into some real-world code.

ruby rack middleware