Two practical examples of Rack middleware in Rails
Friday, January 29th, 2010After Rails moved to Rack as server interface, the ability the use Rack middleware was one of the most touted advantages. At first, it wasn’t very clear to me why this was such a big deal. However, I have applied Rack middleware in the last month on several occasions. I thought it might be interesting for other Rails developers to see some practical examples of middleware, to see where they can be applied.
Redirecting
After a complete rewrite, we released the new version of Floorplanner over a year ago. We had to find a way to deal with the thousands of links that were in use by our customers and indexed by Google.
Some of these could simply redirect to their new location, e.g. http://www.floorplanner.com/tryit can now be found on http://www.floorplanner.com/demo. These redirects can of course simply be implemented in either Rails or Apache.
However, we also had a long list of URLs on which floor plans were published using the old version. These plans cannot be migrated to the new version easily, so we had to keep an instance of the old Rails application running. Requests to these URLs should be redirected to this separate instance, now hosted on a different domain name. Moreover, it is important that it only tries to redirect to the old version if the URL cannot be resolved in the new version, because the plan may have been migrated to or recreated on the new version.
We previously implemented this by adding a catch all route to our application, and a custom handler for the ActiveRecord::RecordNotFound exception in our application. This was a rather ugly solution, and we rather not have a dependency on the old version’s database in the new version’s code.
I decided to rewrite this functionality with a piece of Rack middleware. It basically executes every request with our Rails application, and if it returns a 404 response, it checks if the URL exists on the old version. If so, it redirects the user to the old version, otherwise, it will respond with the 404 response that was generated by Rails.
class RedirectToOldVersionMiddleware def initialize(app) @app = app end def call(env) # execute the request using our Rails app status, headers, body = @app.call(env) if status == 404 && url = find_redirect(env['REQUEST_URI']) # Issue a "Moved permanently" response with the redirect location [301, {"Location" => url}, 'Redirecting you to the new location...'] else # Not a 404 or no redirect found, just send the response as is [status, headers, body] end end REDIRECT_HOST = 'http://old.floorplanner.version' def find_redirect(path) # See if we can find a valid URL on the old version. Redirect.exists?(path) ? "#{REDIRECT_HOST}#{path}" : nil end end
This way, all the code that is related to redirecting users to the old version is contained in a single file, and doesn’t pollute our application anymore. Moreover, when all the old plans are migrated, or we decide we do not longer support these old plans, we can simply disable the middleware in our environment.rb, without having to alter any code.
Logging complete requests for debugging purposes
Another use for Rack middleware is logging: one of our customers notified us that an API call they were using was failing, but only some of the time. Unfortunately, we did not find any errors in our exception log, which made it very hard to debug this issue. After some time, we discovered that Rails was returning a 422 response, without the request ever “arriving” in our own code. Apparently, the request was stopped from processing somewhere in the Rails framework.
Unfortunately, we were not able to recreate this issue ourselves, and we started suspecting that the request was not well-formed, because Rails sends a 422 response in this case. To discover if this really was the issue, I wrote a piece of middleware that logs the complete request, before it is passed to the Rails framework for further processing.
class RequestLoggerMiddleware def initialize(app) @app = app end REQUEST_LOG_DIR = Rails.root.join('log', 'api_calls') def call(env) # log the request if it is a troublesome API call. if env['REQUEST_URI'] == '/failing_api.xml' filename = "api_call_#{Time.now.strftime('%Y%m%d%H%M%S')}.log" File.open(REQUEST_LOG_DIR.join(filename), 'wb') do |file| file.puts(env['rack.input'].read) end end # now, execute the request using our Rails app response = @app.call(env) end end
Using these request log files, API calls can be inspected exactly before any of the Rails magic happens. After resolving the issue, removing this logging was as simple as commenting a line in environment.rb.






