Webapps on App Engine part 3: Request handlers

This is part of a series on writing a webapp framework for App Engine Python. For details, see the introductory post here.

Now that we've covered the background on request handling, it's time to tackle request handlers. Request handlers are the core and most obvious part of a web framework. They serve to simplify the writing of your app, and remove some of the boilerplate that you end up with if you write raw WSGI applications. Before we go any further, let's see what basic request handlers look like in a range of frameworks. Then we can discuss the pros and cons of each, and settle on one for ours.

Django:

def current_datetime(request):
    now = datetime.datetime.now()
    html = "It is now %s." % now
    return HttpResponse(html)

App Engine's webapp framework:

class MainPage(webapp.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.out.write('Hello, webapp World!')

Werkzeug:

@expose('/display/')
def display(request, uid):
    url = URL.query.get(uid)
    if not url:
        raise NotFound()
    return render_template('display.html', url=url)

web2py:

def hello1():
    return "Hello World"

Pylons:

class HelloController(BaseController):

    def index(self):
        # Return a rendered template
        #return render('/hello.mako')
        # or, Return a response
        return 'Hello World'

You've probably noticed that the handlers here fall into two broad categories: Function based handlers, and class based handlers. Frameworks like Django, Werkzeug and web2py take a function-based approach, where each handler is a function. Frequently, such functions are expected to return the response. This matches up fairly closely with the WSGI approach, though frameworks generally add a lot of extra features and syntactic sugar.

Other frameworks, like Pylons and the webapp framework, use class based handlers: each handler is a class, subclassing a base request handler. Requests are handled by instantiating the class, then calling a handler method. In the case of Pylons, multiple different URL patterns may route to the same handler class, resulting in different handler methods being called, while in the case of the webapp framework, the method that gets called is determined entirely by the HTTP method of the request.

For various reasons, we're going to take the class-based approach in our framework. Whilst the function-based approach definitely has its merits, there are several benefits to using a class-based approach in our framework:

  • Easier to understand. Everyone understands inheritance; not as many people understand the magic behind some of the function-based frameworks.
  • Our handler classes can also be valid WSGI apps.
  • It's easier for users of our framework to extend the handler's functionality for their own use.

As far as request routing and dispatching goes, we're going to take the same approach as the webapp framework: The method we call on our handler object depends entirely on the HTTP method of the request. We'll also take the opportunity to make use of WebOb's more advanced features, as we discussed yesterday.

Let's start with the basic functionality we need for our handler to work at all:

import webob
import webob.exc

class RequestHandler(object):
  ALLOWED_METHODS = set(['get', 'post', 'put', 'head', 'delete'])

  def __call__(self, environ, start_response):
    self.request = webob.Request(environ)
    self.response = webob.Response(request=self.request, conditional_response=True)
    try:
      self.handle()
      return self.response(environ, start_response)
    except webob.exc.WSGIHTTPException, ex:
      return ex(environ, start_response)
    except Exception, ex:
      return self.handle_exception(ex)

First, we define a set of allowed HTTP methods. We'll use those shortly. Then, we define the special method __call__, which as we previously discussed, is executed when something attempts to call instances of our class as functions. We take the standard WSGI application arguments, and immediately construct WebOb request and response objects, making sure to set up the response object so it supports conditional responses.

Then, we call the yet-to-be-defined method handle(), which will take care of dispatching the request to the appropriate user defined method. Apart from making the code cleaner, this also allows subclasses to override how requests are dispatched to methods, if they wish, or to add code that is executed regardless of the HTTP method. If handle() returns successfully, we return the response to the user by treating the WebOb response object as a WSGI application.

If the handle() method throws one of WSGI's status code exceptions, we catch that and call it to generate the WSGI response. If some other exception occurs, we catch that and call the handle_exception method, which is expected to do generic error handling for uncaught exceptions.

The handle() method is fairly straightforward, as it only needs to take care of dispatching requests to the appropriate method. Let's take a look:

  def handle(self):
    method_name = self.request.method.lower()
    method = getattr(self, method_name, None)
    kwargs = self.request.environ.get('router.args', {})
    if method_name not in self.ALLOWED_METHODS or not method:
      raise webob.exc.HTTPMethodNotAllowed()
    method(**kwargs)

First, we fetch and normalize the name of the HTTP method. We also extract the keyword arguments that were extracted by the router code back in the first post of the series. Then, we compare it against the list of allowed methods. If it doesn't match, or the method itself isn't found, we simply raise an HTTPMethodNotAllowed exception, which the exception handling code in __call__ takes care of for us. Finally, we call the method in question, passing the router arguments as keyword arguments.

The default implementation of handle_exception() is also fairly straightforward:

  def handle_exception(self, ex):
    logging.exception("Unhandled exception")
    lines = ''.join(traceback.format_exception(*sys.exc_info()))
    return webob.exc.HTTPInternalServerError(detail='%s' % lines)

All we do here is obtain a nicely formatted version of the exception, log it, and print it out. In a real, production-quality framework, you'd definitely want to make sure that in 'production mode', you don't show stacktraces to the users!

Finally, there's one more enhancement we can make to our basic framework to avoid a gotcha that the webapp framework and some other frameworks suffer from: Lack of support for the HEAD method. In webapp, and in our framework so far, if a browser makes a HEAD request, and the user hasn't explicitly implemented it, an HTTP 405 "Method not allowed" response is returned! This has practical implications - some sites such as digg do HEAD requests to check for the existence of a page, so failing to handle it can make it impossible to submit your site to services like digg.

Fortunately, this is easily worked around: All we have to do is provide a default implementation of head() that executes get(), then erases the content of the response. It's not the most efficient, but it is effective:

  def head(self, **kwargs):
    method = getattr(self, 'get', None)
    if not method:
      raise webob.exc.HTTPMethodNotAllowed()
    method(**kwargs)
    self.response.body = ''

The only complication here is that we can't rely on get() being defined in every handler subclass, so we have to use getattr to retrieve it, and throw a regular 405 error if it doesn't exist. Otherwise, we're simply executing get(), then erasing the body of the response before returning.

Let's use our new framework to write a couple of sample handlers:

class HelloHandler(RequestHandler):
  def get(self, name='world'):
    self.response.content_type = 'text/plain'
    self.response.body = 'Hello, %s.' % (name,)

class EchoHandler(RequestHandler):
  def get(self, **kwargs):
    self.response.content_type = 'text/plain'
    self.response.body = repr(kwargs)

router = WSGIRouter()
router.connect("/hello", HelloHandler())
router.connect("/hello/{name}", HelloHandler())
router.connect("/echo/{foo}/{bar:[0-9]+}", EchoHandler(), test="test")

Things are starting to look a little more familiar, right? No more boilerplate, no raw WSGI environments, just familiar and convenient handler classes, request and response objects.

At this point, we've written everything required of a bare-bones webapp framework. Most people expect more to be bundled with their framework, however, and in the next few posts we'll deal with that, starting with the next post, where we'll discuss templating.

Comments

blog comments powered by Disqus