My First Mongrel Handler: A Recipe for providing a JSONP Callback

2 May, 2007

One fact of life is: every technical community has its own recurring bugaboo, an unshakeable criticism that dogs it through its greatest successes, enduring in spite of furious defense and fact marshaling by supporters. Linux? Won't work on the desktop. Lisp? No standard libraries.

In the Rails community our bugaboo is performance. Whether you're a hawk or a dove on the issue, you probably weren't drawn to the framework because of its performance, but because it makes building even complex web apps a joy. You're also probably more than a little sick of hearing about performance in Rails.

But sometimes you're building something dead simple; you may not need Rails' leverage. And sometimes performance may actually be a showstopper. What then?

We encountered both of these conditions recently building a system to provide some JSONP callback functionality. Here's the spec: given a request for the JSON representation of some resource and the name of a callback function, return the JSON wrapped as an argument to the named function. In other words, take a request like:

http://mydomain.com/resource/2d8dbb1119a8.js?callback=myFunction

And return a response in the form:

myFunction({...resource represented as JSON...})

The point is for the user's function to get executed on their page with our JSON as an argument, allowing for data integration without the domain restrictions of AJAX.

Now, what does the code on our side have to do? It has to get the Javascript representation of the resource located at "mydomain.com/resource/2d8dbb1119a8" and then wrap the result in the function named in the "callback" param. That's it. That's the whole feature. All we need to implement it is: access to our data and some kind of structured access to the request itself. And, since this feature is part of our public data API (and so could potentially get called programatically by other people's code) it would be great if it was fast and cheap.

One way to meet these demands would be to write an action in each of our controllers that loaded up the right objects and concatenated their JSON representations with the contents of the callback param to compose the proper text response. But, this would pollute our existing code and use all kinds of unnecessary resources. There must be a better way. If only we had a piece of code lying around that specialized in parsing requests and serving up representations of our resources...

And, of course, we do: Mongrel, everyone's favorite pure-Ruby web server. As a web server, Mongrel speaks HTTP natively; as a kick ass web server, Mongrel is very high performing; and as our web server, it sees all of our requests anyway before they even reach Rails.

At this point, you probably won't be surprised to learn that Mongrel has a feature specifically designed to do this kind of thing: Mongrel handlers. For our purposes, you can think of Mongrel handlers like controllers: they examine the request, do something based on what they find there, and return a response. The Mongrel handler sees the request before it ever reaches our Rails app, so it doesn't have to load up cgi.rb or any of the other resource intensive, performance troubled, parts of the framework. Unlike Rails, It can also handle multiple requests per Mongrel instance. All of which adds up to a major performance win.

So, how would we build our JSONP callback code in a Mongrel handler? This is how:

require 'open-uri'
class JsonpHandler  JsonpHandler.new, :in_front => true

Let's go through this bit by bit starting with the last line. When our script gets loaded up on Mongrel's startup (more nitty gritty on that below) that line tells the server that our handler class should be invoked for any uri that matches 'http://mydomain.com/jsonp/*' (The eagle eyed amongst you will notice that this means the url for requests to this service will have changed slightly from what we mentioned above: http://mydomain.com/jsonp/resource/2d8dbb1119a8.js?callback=myFunction instead of http://mydomain.com/resource/2d8dbb1119a8.js?callback=myFunction. This is the one price we pay for using Mongrel handlers; we need these requests to have something unique after the first slash so that Mongrel can known to intercept them before they reach Rails.). The in_front flag means that Mongrel should check for a match before waking up Rails at all.

When out handler gets invoked, its 'process' method gets called. As you can see, that method takes two arguments: the request and the response. We read the request to figure out what to do, writing the response as we go. We're going to return a response with code 200, so we invoke the start method with that as the argument. That method also takes a block with two local variables representing our response's header and its body (for more info, see the Mongrel::HttpResponse docs).

Inside the block, the first thing we do is set a header: since we're going to be returning executable Javascript, we set the appropriate Content-Type, "text/javascript". Now we get to the real action. We want to get the name of the callback value out of the query, so we grab the query string out of the request's params hash and use the Mongrel::HttpRequest's relevant class method to parse it. The result is a hash with key-value pairs representing everything after the "?" in our url. We pull the callback out of that and remember it.

Now, we've gotten the name of the callback function out of the request, so we're halfway there. All we've got left is to get the JSON representation of our resource. We've got two choices: we could load up our actual Rails objects or, we could use their urls. If we wanted to go the first route, we'd end up using Merb, a whole microframework that combines Mongrel handlers with ActiveRecord and ERB templates to provide a complete lightweight alternative to Rails. We don't need to go that far. All we've got to do is make a request for our own resource's .js url and we'll have what we need. At first glance, this may seem to be a security flaw (since it causes us to grab a url that is interpolated from a request that comes in), but we're only going for urls on our own domain, and we're just storing their contents in a string and then returning them to the agent who asked. The worst someone could do would be to request an image file or other expensive resource, which would then be garbled by having some text prepended to it and returned to them. We use open/uri to request the resource: DEPLOY_DOMAIN is a project-wide constant (bascially localhost or our real domain) and request.params["PATH_INFO"] gives us the request from the first slash up to the '?' -- just what we need to construct request for our resource. (There may be some performance downside from having an open/uri call in the midst of a Mongrel handler, but I don't know nearly enough about threading to speak intelligently on the subject.)

The last line of the block just puts the pieces together and writes the result as the response's body.

Once we've got this code written all we've got to do is tell Mongrel to load it up on launch:

$mongrel_rails start -S path/to/jsonp_handler.rb

This post would not have been possible without:
the slides from ezmobius's Merb/Mongrel talk. Read them for more (and much better informed) info on the subject.

Tagged: , , , , ,