Understanding how Rack uses Ruby lambdas

Rack has proven to be a great way for developers to quickly modify the request/response cycle for Ruby web applications.

Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call.

This is all fine-and-good, pretty standard Ruby stuff. Rack works well when you start chaining these together. Rack responses return an array of three items: the status, headers, and response body: [status, headers, response_body]. Let's setup a starting point for our response (env) and couple of basic Rack style lambdas.

We lost the header changes because the body_lambda is strictly returning {"Content-Type" => "text/plain"}. This is a gotcha to be aware of when writing middleware. At any point in the chain you can completely reset any part of the return value. You need to be sure to pass on things that should be passed on and only update things your middleware is responsible for. Lets fix body_lambda so that it will pass along any response headers:

Rack and other similar tools allow lambdas to be chained together in order to provide a generic framework for handling the request/response. The meat of this is the Enumerable inject method. The inject method will walk over an Enumerable applying a method to the results of the previous iteration and return the final iteration. In our case we pass our env array as a starting point.

Both of our lambdas expect an array to do their work on, so we give them the env array as a starting point. This is important because without it every lambda would have to deal with cases in which there is no value passed.

Creating classes to use as middleware

Rack puts chaining lambdas together by providing a wrapper class to build an array of lambdas. Before we get into building a wrapper, let's take a look at how we can build classes that represent our header_lambda and body_lambda from above.

So h represents the lambda containing Header#new, which expects a lambda representing the current state. The response here is the Header class inside lambda { |app| Header.new(app) }. We have to send the call method again to get the response. But notice that Header#call uses @app.call(env) to set the responses. At this point, it doesn't matter what we send the call method.

Why then should we use the call method in the Header or Body classes? Doesn't it seem to add unnecessary confusion? I think it makes the internal code more confusing, but it also makes the API it provides pretty simple. Let's use the inject method used above to put this together.

It's not pretty on the console, but the inject method has returned an instance of the Body class containing an instance of Header which in turn contains our inital_response_lambda. Sending call to this with any parameter will use the Body instance to unravel the whole stack.