Rack Middleware and Rack Applications: What’s the Difference?

Rack is the common Ruby web infrastructure powering just about every framework known to Ruby-kind (Rails, Sinatra and many more). Rack Middleware are a way to implement a pipelined development process for web applications. They can do anything from managing user sessions to caching, authentication, or just about anything else. But one thing that confused me for a long time: what’s the difference between a Rack application and Rack middleware? Both are ostensibly the same, something that responds to #call(env), so how are they different? Unlike Rack applications, Rack middleware have knowledge of other Rack applications. Let’s take a look at what this means with a few simple examples.

The simplest Rack application (in class instead of lambda form) would be something like this:

class RackApp   def call(env)     [200, {'Content-Type' => 'text/plain'}, ["Hello world!"]]   end end

Note that because the method required by the Rack specification is call and (by no coincidence) this is how you execute Procs and lambdas in Ruby, the same thing can be written like so:

lambda{|env| [200, {'Content-Type' => 'text/plain'}, ["Hello world!"]]}

This hello world app would simply output “Hello world!” from any URL on the server that was running it. While this is obviously simple, you can build entire powerful frameworks around it so long as in the end a request boils down to a status, some headers, and a response body. But what if we want to filter the request? What if we want to add some headers before the main application gets it, or perhaps translate the response into pig latin after the fact? We have no way to say “before this” or “after that” in a Rack application. Enter middleware:

class RackMiddleware   def initialize(app)     @app = app   end      def call(env)     # Make the app always think the URL is /pwned     env['PATH_INFO'] = '/pwned'     @app.call(env)   end end

See the difference? It’s simple: a Rack middleware has an initializer that takes another Rack application. This way, it can perform actions before, after, or around the Rack application because it has access to it during the call-time. That’s why this works in a config.ru file:

run lambda{|env| [200, {'Content-Type' => 'text/plain'}, ["Hello world!"]]}

But this does not:

use lambda{|env| [200, {'Content-Type' => 'text/plain'}, ["Hello world!"]]}

Because the ‘use’ keyword indicates a Middleware that should be instantiated at call-time with the arguments provided and then called, while ‘run’ simply calls an already existing instance of a Rack application.

This is something that can be quite confusing, especially if you’re new to the Rack protocol, so hopefully this clears it up a bit!