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.
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:
- All Middleware classes have an
initialize
method that takes an application as an argument. In that method we’ve created an instance variable called@app
so that we can reference the application in other methods. In this case@app
will be a reference to our Rails application. ResponseTimer
is a Rack application in itself, although we’ll be making it act like a filter for our Rails app. All Rack applications have acall
method that takes an environment hash variable as an argument and returns an array containing an HTTP status, a hash of headers and a response object. For now ourcall
method is just going to return a simple “Hello, World!” response to make sure that everything is working as we’d expect it to.
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.
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.