Intro into Rack .
February 05, 2017

A look into how Rack works
This weeks writing topic is to understand how Rack works. Rack provides a standardised interface with the web server and is used by a number of web frameworks including Sinatra and Rails.
I had previously been aware of Rack, and how it was the back bone of Rails, but hadn’t put in the hard yards to understand it. This post outlines how it intuitively works and hopefully removes some of the mystery surrounding Rack.
What is Rack
Imagine that your web application is made up of different layers. These layers will have different responsibilities from authentication, to adding content. A web request then goes through these layers and the resulting response is returned.
Having a standardised set of rules that each layer must comply with allows us to develop “new” layers that will “just work” with the existing layers as long as it complies with this standard. This standardised rules is know as Rack. As it turns out, the standard is remarkably simple to implement.
Rack Specification
The Rack specification requires that the middleware must have a instance method called call and unless it is the last middleware in the queue must stores a reference to the next middleware. The ‘call’ instance method takes an environment hash as a parameter, and returns an Array with three elements.
These elements are:
- The HTTP response code
- A Hash of headers
- and a response body
The layers are linked together via the call method. The middleware of the first layer will execute the call method of the next layer using the stored reference. While that call method is being executed it also executes the call method of the next layer, and so on.
(See github.io for more details)
Seeing is believing
I found walking through an example helped me understand how it all fits together. The code below has been modified to aid understanding. If you want to see an unadulterated version then head to the file /lib/rack/builder.rb of the rack gem.
class Header
def initialize(app) @app = app; end
def call(env)
status, headers, response = @app.call(env)
[status, headers.merge({"X-Authorization" => "secret"}), response]
end
end
class Body
def initialize(app) @app = app; end
def call(env)
status, headers, response = @app.call(env)
[status, headers, ["#{response} from Body"]]
end
end
class Builder
def initialize(default_app = nil, &block)
@use, @map, @run, @warmup = [], nil, default_app, nil
instance_eval(&block) if block_given?
end
def use(middleware, *args, &block)
@use << proc { |app| middleware.new(app, *args, &block) }
end
def to_app
app = @run
app = @use.reverse.inject(app) { |a,e| e[a] }
app
end
def call(env)
to_app.call(env)
end
def run(app)
@run = app
end
end
builder_app = Builder.new {
use Header
use Body
run lambda { |env| [200, {}, ""] }
}
builder_app.call({}) #environmental variables from server
=> [200, {"X-Authorization"=>"secret"}, [" from Body"]]
Specification Middleware
Let’s take the above code apart by looking at how the builder object is instantiated (created)
builder_app = Builder.new {
use Header
use Body
run lambda { |env| [200, {}, ""] }
}
The Builder.new receives a block that specifies which middleware is used in the rack stack and in the order that they are applied . In the case above, the #use method adds the Header and Body middleware. These middleware are required to have a call instance method, and to store a reference to the next middleware in the queue.
The last middleware in the queue is a special case since it doesn’t need to store a reference to the next middleware. It gets added to the rack stack using the above run method. Its only requirements is that it has a call method. If you are wondering, this can be a lambda object, as seen in our example, since that gets executed with a call method.
How rack gets used?
We have finally arrived at how Rack gets used in our application. A server request comes in from our website. The server then requests a response from our application by executing the following code. This is the entry point into our application, or ground zero.
builder_app.call (environmental hash)
What happens with builder_app.call
The above code returns the application response (status, body, headers) to the server and also creates the initial rack stack
builder_app.call (environmental hash)

In our example, the builder_app references an instance of the Builder. This specifies the middleware stack while it is instantiated (see above Specification Middleware)
Thea above method runs the code to_app#call(env)does two things (step 1).:
- The first part
to_appmethod instantiates the rack stack (using the middleware specified inBuilder.new) . - The second part
#callexecutes the call method on the instantiated rack stack
Lastly, the call method starts off the chain reaction by executing the first middleware’s call method in the rack stack . This in turn, executes the call method of the next middleware in the rack stack, and so on.
Instantiating the middleware stack
In our example, there are three middleware layers: Header, Body and Lamba object. The @use.reverse.inject step ineffectively combines the code as Header.new(Body.new(app))) using ruby’s inject method.
def to_app
app = @run
app = @use.reverse.inject(app) { |a,e| e[a] }
app
end
The last middleware (lambda object) has no need to store the next middleware in the queue. It only is required to have a call method that returns an array of status, headers and body.
If it is not last, the middleware stores a reference to the middleware next in queue. In our example, the header middleware stores a reference to the body middleware (step3) and the body middleware stores a reference to the lambda (step 4)

What is important here is that the middleware are linked together by storing a reference to the middleware next in queue.
The call method
After the code builder_app.call (environmental hash) has created the rack stack it then executes the call method against it. The header object#call is initially executed, but within that method it kicks off the call method of the next middleware using its stored reference @app (step 7)

Within that method, thebody object’s call method is executed (step 8) and finally within this call method the last middleware’s call method is executed (step 9).
Phew ! The callmethod, for the last middleware object, completes and the process steps backwards through the call sequence. At each stage, the status, header, and body is returned from each call method. From the lambda#call, the focus returns to the body#call method and on that methods completion to header#call and finally the application response is sent back to the web server and closes the circle.
At each of these layers, the middleware can manipulate the status, header and body fields on the down stream and modify the environment variables passed up stream to higher middleware layers.
The resulting response from our program is :
status: 200, header: {"X-Authorization"=>"secret"} and body: " from Body"