homeASCIIcasts

151: Rack Middleware 

(view original Railscast)

One of the major changes to Rails 2.3 is that it is now uses Rack. Rack is a beautiful thing, it provides a standardised interface for a web server to interact with a web application. This might seem a fairly simple thing, but it gives us a lot of power. One of the things it enables is Rack Middleware which is a filter that can be used to intercept a request and alter the response as a request is made to an application.

The previous episode demonstrated Rails Metal, which is similar to Middleware in that it uses the Rack interface to handle a request. The main difference is that Metal acts as the endpoint for a request and returns a response, whereas Middleware acts like a filter and can pass the request on to an application, then filter the response that is returned.

To demonstrate Rack Middleware we’re going to use the simple e-commerce application seen in previous episodes. We’d like to know how long each request takes to process so we’re going to use Middleware to inject an HTML comment into each page that contains the processing time. This is probably not something you’re going to want to do in a production Rails application, but it will provide a good demonstration on how Middleware can intercept a request and response and change an application’s behaviour.

Our e-commerce app.

Creating The Middleware

A Middleware filter is just a Ruby class. We’ll create a class under the lib folder of our Rails application in a file called response_timer.rb. The code for the class is shown below.

class ResponseTimer
  def initialize(app)
    @app = app
  end
  def call(env)
    [200, {"Content-Type" => "text/html"}, "Hello, World!"]
  end
end

Our first Middleware class.

There are a couple of things of note in the class above:

There’s one more change we’ll need to make before our Middleware will be called. In the Rails application’s environment.rb file we have to add a line in the Rails::Initializer.run block so that the app knows to treat our new class as Middleware.

Rails::Initializer.run do |config|
  config.middleware.use "ResponseTimer"
end

We’ll have to pass the name of the class as a string rather than as a constant because the class will not be loaded when the application is initialised. Passing the name as a string ensures that the class is loaded when the Middleware is set up.

To test that our new class is being picked up as a Middleware class we can call rake middleware. The class should be listed in the output.

eifion$ rake middleware
(in /Users/eifion/rails/apps_for_asciicasts/ep151)
use Rack::Lock
use ActionController::Failsafe
use ActionController::Reloader
use ActionController::Session::CookieStore, #<Proc:0x01b90eb4@(eval):8>
use ActionController::RewindableInput
use ActionController::ParamsParser
use Rack::MethodOverride
use Rack::Head
use ResponseTimer
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
run ActionController::Dispatcher.new

All of the Middleware our application is using is listed above, including our ResponseTimer class.

Testing The Middleware

Now that our Middleware class is in place we can test it. Every time we make a change to the class we’ll have to restart the server. Once we’ve done that and we go back to our products page we’ll see the output from our Middleware class instead of the page we saw before.

The response from our Middleware class

The response from our Middleware class.

It isn’t just the products controller that is intercepted by the Middleware. We can call any action in any controller and we’d see the same output, even if we call an action that doesn’t exist. This happens because our Middleware isn’t acting as Middleware right now; it is catching every request and returning the same response so our Rails application isn’t even seeing the requests. We want to filter the responses from our Rails applications rather than just display “Hello, World!” so we’ll now modify our Middleware class to do that.

class ResponseTimer
  def initialize(app)
    @app = app
  end
  def call(env)
    status, headers, response = @app.call(env)
    [status, headers, "<!-- Response Time: ... -->\n" + response.body]
  end
end

Instead of just returning a static response, we want to return a modified version of the response from our Rails application. As our Rails app is a Rack application and we have a reference to it in the @app instance variable we can call call on it, passing through the environment variables, and modify the response. Like any other Rack application there will be an array returned containing the status, the headers and a response object. The response will be an ActionController::Response object, rather than a string, so we’ll have to call its body method to get the response as a string. We’ll add an HTML comment to the beginning of the body before returning it.

When we look at our products page again (after restarting the server) we’ll see the comment at the top of the page’s source code. Our original aim was to have the time the response took in the comment. We’ll modify the call method now to do that.

def call(env)
  start = Time.now
  status, headers, response = @app.call(env)
  stop = Time.now
  [status, headers, "<!-- Response Time: #{stop - start} -->\n" + response.body]
  end

After restarting the server again, we’ll now see the response time in the comment.

<!-- Response Time: 0.021467 -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

Filtering The Content Type

One problem with our Middleware class is that it assumes that every response is an HTML document, which is not always the case. To make sure that we only add our timer comment to the appropriate documents we can check the Content-Type header.

def call(env)
    start = Time.now
    status, headers, response = @app.call(env)
    stop = Time.now
    if headers["Content-Type"].include? "text/html"
      [status, headers, "<!-- Response Time: #{stop - start} -->\n" + response.body]
    else
      [status, headers, response]
    end
  end
end

The call method will now pass the response straight through for any content type other than text/html.

Configuring Middleware

If we want to pass parameters to our Middleware we can. Say we want to customise the message that we return in our HTML comment. We can do this by passing additional parameters to the Middleware class when it is created.

class ResponseTimer
  def initialize(app, message = "Response Time")
    @app = app
    @message = message
  end
  def call(env)
    start = Time.now
    status, headers, response = @app.call(env)
    stop = Time.now
    if headers["Content-Type"].include? "text/html"
      [status, headers, "<!-- #{@message}: #{stop - start} -->\n" + response.body]
    else
      [status, headers, response]
    end
  end
end

In our initialize method we’ve added another argument called message, with a default value. Its value is stored in an instance variable called @message. In the call method we put the message into the HTML comment.

To set the message we have to alter the line of code in environment.rb where we defined our new Middleware class. We just pass the message as a second argument.

Rails::Initializer.run do |config|
  config.middleware.use "ResponseTimer", "Load Time"
end

After another restart and refresh the comment at the top of our page will now be updated to show our new message.

<!-- Load Time: 0.179821 -->

Improving The Middleware

What we’ve done so far works perfectly well, but it’s not using Middleware in the best way. We’re using response.body which is a method specific to Rails applications and which assumes that the response object is an ActionController response. When working with Rack Middleware we shouldn’t assume that we’re dealing any specific framework or application.

A way to make our Middleware class work with any Rack application would be to use response.each which takes a block and to add our content within the block. This approach can become messy fairly easily, though so we’re going to restructure our Middleware class. We’ll show the code first and then explain it.

class ResponseTimer
  def initialize(app, message = "Response Time")
    @app = app
    @message = message
  end
  def call(env)
    @start = Time.now
    @status, @headers, @response = @app.call(env)
    @stop = Time.now
    [@status, @headers, self]
  end
  def each(&block)
    block.call("<!-- #{@message}: #{@stop - @start} -->\n") if @headers["Content-Type"].include? "text/html"
    @response.each(&block)
  end
end

The first thing to note is that in the call method we’re returning self in the array that we pass back to the Rack application instead of the response. The only method that Rack requires a response object to support is each. This makes our ResponseTimer class a type of response. We’re also making start, stop, status, headers and response instance variables so that we can access them in the each method.

Each iteration through the each method’s block will return some content that will be returned to the user. The first iteration will return our timer comment if the content type is text/html and all other iterations will be passed back to the response object.

With one last restart and refresh we’ll check the source of our application and see if the timer comment still works.

<!-- Load Time: 0.023725 -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>

It does! Now that we’ve rewritten our Middleware it can be used with any type of application that supports Rack.

Other Uses For Middleware

Our example, while it demonstrates how to use Middleware, isn’t the most useful application of it. There are though, unlimited uses for Middleware. For more example Middleware applications, take a look at the rack-contrib project on GitHub. There are many Middleware applications there that you can use in your own projects or browse through to see how to make your own Middleware apps.