Web Development in Ruby : Middleware in Rack Apps

Objective


To learn about Middleware.

Discussion


Middleware is a general term for any program that serves to glue together two separate programs. It is the software that connects two separate applications and passes data between them. Alternatively, software that allows more than one application to share information seamlessly.

Rack filters allow on the fly transformations of payload and header information in both the request into a resource and the response from a resource.

A Rack application can transform the content of HTTP requests, responses and header information. They can modify or adapt the requests for a resource and modify and adapt responses from a resource.
Rack filters can intercept :

• The accessing of a resource before a request to it is invoked.
• The processing of the request for a resource before it is invoked.
• The modification of request headers and data by wrapping the request in customized versions of the request object.
• The modification of response headers and response data by providing customized versions of the response object.
• The interception of an invocation of a resource after its call. Here is some examples on how Rack filters could be used:
• Authentication
• Logging and auditing
• Image conversion
• Data compression
• Encryption
• Tokenizing
• Filters that trigger resource access events
• XSL/T filters that transform XML content
• MIME-type chain filters
• Caching
• Exception Handling

Examples of Rack Middleware that comes with Rack distribution:

Rack::Static - Handles fetching and returning static assets or caches
Rack::Session::Cookie - Handles your sessions via cookies
Rack::ShowExceptions - Catches any exceptions thrown in your app and returns a useful back-trace on the exception and many more.

Steps


Step 1

In a previous article we wrote the code that outputs header and footer for the content generated by the Rack app. Here it is reproduced again for your convenience:

require 'rack'
require 'thin'

rack_app = lambda do |env|
  request = Rack::Request.new(env)
  response = Rack::Response.new

  response.write '--------------Header-------------<br/>'

  if request.path_info == '/hello'
    response.write 'Saying hi '
    client = request['client']
    response.write "from #{client}" if client
  else
    response.write "You need to provide the client information"
  end
  response.write "<br/>--------Footer-----------------"
  response.finish
end

Rack::Handler::Thin.run rack_app, Port: 3025

Step 2

We are going to split this into two Rack apps that will co-operate to produce the same output as the above program. First step is to remove the footer and header generation out of the above program so that it now becomes:

require 'rack'
require 'thin'

rack_app = lambda do |env|
  request = Rack::Request.new(env)
  response = Rack::Response.new

  if request.path_info == '/hello'
    response.write('You said hello')
    client = request['client']
    response.write("from #{client}") if client
  else
    response.write 'You need to provide some client information'
  end
  response.finish
end

Rack::Handler::Thin.run Decorator.new(rack_app), Port: 3025

Step 3

Compare this code to the program in the previous step, you see that the last line that invokes the run is now using Decorator class. This class will generate the header and footer. Let's define the Decorator class which will take the original Rack app as an argument to its constructor. The class will look something like:

class Decorator
  def initialize(app)
    ...
  end
  def call(env)
    ...
  end
end

Step 4

Now look at the implementation of Decorator below. Notice how it wraps the response body from the original response body from the Rack app that was passed into its constructor.

class Decorator
  def initialize(app)
    @next_in_chain = app
  end
  def call(env)
    status, headers, body = @next_in_chain.call(env)
    new_body = "---------------Header--------------<br/>"
    body.each{|s| new_body << s}
    new_body << "<br/>---------Footer-------------------"
    headers['Content-Length'] = new_body.bytesize.to_s
    [status, headers, [new_body]]
  end
end

Discussion


The line number 34 now passes an instance of Decorator, another Rack app as the first argument. The constructor of Decorator takes a Rack app and it modifies the body of the response to print the header and the footer thereby wrapping the output of the Rack app that was passed to it in the constructor. This produces the same output as the example in previous article which prints You said hello enclosed by the header and footer.

Decorator is a valid Rack application since it meets the Rack spec. As a general rule, any middle-ware must be a valid Rack application.

When the Thin server is run it calls the call method of the Decorator. In line 6 you see that our original Rack app gets called by the Decorator. This call to original Rack app returns the status, headers and body which gets initialized in the local variables.

The status is an HTTP status. When parsed as integer (to_id), it must be greater than or equal to 100.
The header must respond to each and yield values of key and value. The header keys must be Strings. The header must not contain a Status key, contain keys with : or newlines in their name, contain key names that end in hyphen or underscore, but only contain keys that consist of letters, digits, underscore or hyphen and start with a letter. The values of the header must be Strings, consisting of lines (for multiple header values, e.g. multiple Set-Cookie values) separated by \n. The lines must not contain characters below 037.

There must be a Content-Type, except when the Status is 1xx, 204 or 304, in which case there must be none given. There must not be a Content-Length header when the Status is 1xx, 204 or 304.

The Body must respond to each and must only yield String values. The Body itself should not be an instance of String, as this will break in Ruby 1.9. If the Body responds to close, it will be called after iteration. If the Body responds to to_path, it must return a String identifying the location of a file whose contents are identical to that produced by calling each; this may be used by the server as an alternative, possibly more efficient way to transport the response. The Body commonly is an Array of Strings, teh application instance itself, or File-like object.

Coming back to our example, we only know that the original body can respond to each, which returns a String. Therefore we call each on the body and append the old string values from the original Rack app body to the header and append the footer to the end to create a new_body. This happens in 7, 8 and 9 in the code shown above.

Let's insert puts body.inspect before the line 8 and run the app. In the console we get the output of inspecting body:

#<Rack::Response:0x00000100bae1d0 @status=200, @header={"Content-Type"=>"text/html", "Content-Length"=>"38"}, @writer=#<Proc:0x00000100badf00@/usr/local/lib/ruby/gems/1.9.1/ gems/rack-1.2.1/lib/rack/response.rb:27 (lambda)>, @block=nil, @length=38, @body=["You did not provide client information"]>

Well, this is a surprise. You might have expected the type to be String but it is an instance of Rack::Request. If you refer the documentation for Rack::Response you will see that it responds to each method and returns String thereby satisfying the Rack specification that the Body must respond to each and only yield String values. It also says that the Body commonly is an Array of String, the application instance itself or a File-like object.

Step 5

Let's take a closer look at line number 30:

response.finish is implemented in lib/rack/response.rb as shown below:

def finish(&block)
  @block = block

  if [204, 205, 304].include?(status.to_i)
    header.delete CONTENT_TYPE
    header.delete CONTENT_LENGTH
    close
    [status.to_i, header, []]
  else
    [status.to_i, header, BodyProxy.new(self){}]
  end
end

Step 6

As you can see the body value is an instance of Response object itself. Let's look at the implementation of each for Response (in lib/rack/response.rb):

def each(&callback)
  @body.each(&callback)
  @writer = callback
  @block.call(self)  if @block
end

The each method delegates the work to the body variable of the Request object. In line number 10 we make sure that we set the value for Content-Length.

Middleware is framework-independent components that process requests independently or in concert with other middleware.

Between the server and the framework, Rack can be customized to your applications needs using middleware, for example:

Rack::URLMap to route to multiple applications inside the same process. Rack::CommonLogger for creating Apache-style log files.
Rack::ShowException, for catching unhandled exceptions and presenting them in a nice and helpful way with clickable backtrace.
Rack::File, for serving static files.
Rack::Cascade - Try a request with several apps, and return the first non-404 result ...many others!

All these components use the same interface, which is described in detail in the Rack specification. These optional components can be used in any way you wish. You can find the list of Rack built-in middleware on it's home page.

Why Middleware

Separation of Concerns is the process of separating a program into distinct features that overlap in functionality as little as possible. A concern is any piece of interest or focus in a program. It is achieved through modularity of programming and encapsulation with the help of information hiding. Layered designs in information systems are also based on separation of concerns (presentation, business logic, data access, database etc).

If we are attempting to separate concern A from concern B, then we are seeking a design that provides that variations in A do not induce or require a corresponding change in B (and the converse). If we manage to successfully separate those concerns then we say that they are decoupled.

Most programs require some form of security and logging. Security and logging are often secondary concerns, whereas the primary concern is often on accomplishing business goals. The core concern is the business logic. Security and logging are cross-cutting concerns.

The middleware can achieve separation of business logic from other common logic. These common logic can be used in a different business context. For instance the same security and logging functionality can be applied to a financial domain as well as medical domain. This enables the core domain model to evolve independent of other models such as security and logging resulting in reuse and ease of maintenance.

Step 7

Decorator we wrote earlier can be used with any Rack application.

class Decorator
  def initialize(app)
    @next_in_chain = app
  end
  def call(env)
    status, headers, body = @next_in_chain.call(env)
    new_body = "---------------Header--------------<br/>"
    body.each{|s| new_body << s}
    new_body << "<br/>---------Footer-------------------"
    headers['Content-Length'] = new_body.bytesize.to_s
    [status, headers, [new_body]]
  end
end

rack_app = lambda do |env|
  [200, {'Content-Type' => 'text/html'}, ['whatever rack app']]
end

Rack::Handler::Thin.run Decorator.new(rack_app), Port: 3025

This is a simple example. Suppose we had implemented a middleware for user authentication, then the middleware can be used with any Rack application without any modification. Develop middleware that is focused on doing one thing really well. The advantages are :

a) Each middleware can be developed independently and used in a plug-n-play manner.
b) We can mix and match middleware to combine them in several ways to meet the needs of different applications and can be dynamically configured based on application needs.

Summary


In this article, you learned the what, why and how to use Middleware in a Rack app.


Related Articles


Create your own user feedback survey