python Python Give your WSGI application super-powers

Hulk Simpson

I agree that our last week "Hello World" application was not very impressive.

In fact I don't think any "Hello World" can be impressive at all, unless it has some mighty not-hello-world-at-all features.

Let's sum up what we have so far. We have a python script, that spawns a webserver on port 2222 and answers HTTP requests saying hello.

For a ten line script, it's not so bad, but still hello-word-ish.

Baah.

About middlewares

This diagram is part of the Pylons Project documentation

WSGI introduce the concept of "middlewares" (Les wares du milieu).

It is, like WSGI, a very simple concept. As all your WSGI applications have the same prototype, it's very easy to create decorators that would be reusable WSGI application enhancers.

The picture on right, which comes from the Pylons framework documentation, explain it graphically. Of course the layers presented here are only an example from the said framework, and your setup is only limited by your immagination.

This is not unique to python, nor revolutionnary. The "middleware" concept is pretty similar to ruby's rack or to symfony's filter chain.

The most amazing middleware ever: ajax interactive debugger

Let me introduce a new friend, weberror.

At a first glance, weberror interface may look like any other stack trace dumper from <insert-your-favorite-language-or-framework-here>.

But if you take a few seconds to explore it, you will get why it's 100 times more powerfull. The huge difference is that you can expand a frame (using the little plus icon) to get an interactive python interpreter in the context of the stack frame.

Type some python ...
Feel like you're within your application.

This is great, but looks like a hell to integrate in some home-made application... Actually it is not! Weberror contains a WSGI middleware that will enpower any WSGI compliant application with its features. Let's add it to our last week's application.

from webob import Response as HttpResponse
from webob.dec import wsgify
from weberror.evalexception import EvalException as AjaxDebuggerMiddleware

@wsgify
def application(request):
    if request.path_info == '/error':
        raise Exception('Guess what, an error happened!')

    return HttpResponse('Hello, debuggable world at %s.' % request.path_info)

application = AjaxDebuggerMiddleware(application)

if __name__ == '__main__':
    from gevent import wsgi
    wsgi.WSGIServer(('', 2222), application, spawn=None).serve_forever()

Note that the feature costs 2 line of python (one import, and the decoration of our original app). We added 2 more lines to raise an error if the URL is /error.

Please note that doing this opens a huge backdoor in your application, as any uncaught exception will give the http client a python shell on your box. Be responsible with what you're doing, or you'll have problems. Huge ones.

To avoid the usual mistake of running the app bundled with the debugger unless we really want to, let's add a --debug command line option that enables it.

We are using optparse module which is deprecated as of Python 2.7. You should consider using the argparse module if you're using a recent Python version.
from webob import Response as HttpResponse
from webob.dec import wsgify

@wsgify
def application(request):
    if request.path_info == '/error':
        raise Exception('Guess what, an error happened!')

    return HttpResponse('Hello, debuggable world at %s.' % request.path_info)

if __name__ == '__main__':
    from gevent import wsgi
    from optparse import OptionParser

    parser = OptionParser()
    parser.add_option("-D", "--debug", dest="debug", action="store_true",
            default=False, help="activate the interactive web debugger")

    options, args = parser.parse_args()

    if options.debug:
        from weberror.evalexception import EvalException as AjaxDebuggerMiddleware
        application = AjaxDebuggerMiddleware(application)

    wsgi.WSGIServer(('', 2222), application, spawn=None).serve_forever()

You can now try the new features.

$ python middleware2.py --help
Usage: middleware2.py [options]

Options:
  -h, --help   show this help message and exit
  -D, --debug  activate the interactive web debugger

Ok, and now what ?

Now you've seen one of the most stunning middleware that exists, let's think of other cool usage our framework may need. Note that I'm not claiming all of those features are actually good ideas to be implemented as middleware, but they are implementable this way.

  • Middlewares can intercept requests ...
  • One can route requests between several WSGI applications (subdomains ?)
  • One can handle HTTP cache using HTTP headers. If a cached version is there, the request won't go to the next WSGI layer.
  • Middlewares can intercept outgoing stuff ...
  • One can consider logging unhandled errors on a prod server, or even sending mails about them.
  • One can look at the request Accept header and provide content type adapters for unacceptable types. You don't know about XML ? Let me present you an HTML view of this file.
  • Middlewares can put additionnal informations in the WSGI environment dict.
  • One can handle session storage/loading.
  • And much more ...

The drawback of using middlewares is that it adds complexity to your application factory (the callable responsible for building the wsgi app).

As soon as you want to switch components painlessly, middlewares are starting to get in your way. You probably won't ever change the debugger, only enable or disable it, so the middleware solution is fine.

But what about session handling for example ? Or cache backends ? It's completely possible to use middlewares, and you may have a look at beaker that contains middlewares for caching and sessions.

However, we'll add those features using a much more flexible technique very soon.

Share the love!

Liked this article? Please consider sharing it on your favorite network, it really helps me a lot!

You can also add your valuable insights by commenting below.