Understanding Rack Middleware
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 prettieruse
andrun
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.