Intro into Rack .

February 05, 2017

Image 16

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)

Builder create rack middleware

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 in Builder.new ) .
  • The second part #call executes 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)

Builder reverse inject

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)

Through the rack middleware

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"

Websites