A Better webob.exc

In case you don’t know what webob.exc is, it’s a module that contains classes for every possible HTTP status code — all those 2xx, redirects and errors. The clever thing about it is having all those response classes inherit from both webob.Request as well as Exception. The point of that is so that we can wrap code in try .. except HTTPException and then override responses from anywhere inside that block — even from deep down the stack.

This in turn allows us to implement authentication and authorization in such a way that the permission checks can happen anywhere and they do not require much cooperation from the entire call stack to display a “403 Permission Denied” or a login form when necessary. This example from the docs should give you an idea how it can be used.

Something like this:

def __call__(self, environ, start_response):
    req = Request(environ)
    try:
        resp = self.process(req)
    except exc.HTTPException, e:
        resp = e
    return resp(environ, start_response)

OverrideResponse

So this seems pretty cool and useful, however there’s a cleaner way to accomlish the same thing. I’ve been using a slightly modified approach for some time and I assure you it works great. First of all, I separated the Exception part, so instead of inheriting from it, here’s what I have:

class OverrideResponse(BaseException):
    def __init__(self, app):
        self.app = app

And it’s used like this:

def __call__(self, env, start):
    try:
        app = self.func(Request(env))
    except OverrideResponse, exc:
        app = exc.app
    return app(env, start)

To override response from down the stack, one can use this:

raise OverrideResponse(HTTPForbidden())

That very, very similar, and the raise part looks more verbose even, so what’s the point?

First reason I like this better is that we can use any WSGI app as the override. (One can do raise HTTPException("override", other_app) as well, but that’s another thing one has to know to accomplish almost the same task — not good.)

The second reason is more important. If we remove Exception from the baseclasses of all those error responses application code becomes more readable — when we instantiate an HTTPException subclass we might use it directly or maybe we are preparing it to raise later. After the separation we can immediately make better guesses about control flow in the app.

This is subtle but quite useful. For example, to find all the places we use the exception overriding trick, we now can search code for OverrideResponse.

Third reason is that we have webob.exc.HTTPOk, HTTPTemporaryRedirect and all those other classes that can be raised as exception even though it makes no sense to do it. In fact, if you think about it, there’s no reason to ever use HTTPOk, it’s only there for completionist’s sake.

Moving exception-override functionality into just one dedicated exception class just makes more sense and exposes the next issue.

Fewer Classes

While all those classes create the impression that “wow, a lot of work was done for me already”, in practice they don’t differ much. So I replaced all of those classes with just two: RedirectResponse and ErrorResponse. (I just dropped all the 2xx ones).

Redirect Responses

Let’s look at the redirects first. All of them function in pretty much the same way. They are never seen by the user so all of the templating going on in webob.exc is unnecessary. Creating a response is a matter of supplying the specific HTTP status code and the target URL.

return RedirectResponse(301, req.host_url)

Rememebering the status codes is not necessary though, because the app accepts string aliases (perm, temp, see, found).

return RedirectResponse('permanent', req.host_url)

Here’s the implementation:

_redirect_types = {
    'permanent': 301,
    'perm': 301,
    'found': 302,
    'see other': 303,
    'see': 303,
    'temp': 307,
}

class RedirectResponse(object):
    def __init__(self, status, location, message=None):
        if not isinstance(status, int):
            status = _redirect_types[status]
        self.status = status
        self.location = unicode(location)
        self.is_absolute = is_absolute(self.location)
        self.message = unicode(message or u"%s %s" % (status, status_reasons[status]))
        if self.is_absolute:
            self.response = self.make_response(self.location)

    def make_response(self, location):
        return Response(
            u'%s\n%s' % (self.message, location),
            self.status,
            ctype=_ctype_text,
            charset='UTF-8',
            location=location,
        )


    @wapp
    def __call__(self, req):
        if self.is_absolute:
            return self.response
        location = unicode(req.app_uri.join(self.location))
        return self.make_response(location)

Error Responses

Error reponses work almost the same way, but we want to be able to generate text or html error messages depending on the user-agent.

>>> print Request.blank('/').get_response(ErrorResponse('forbidden'))
403 Forbidden
Content-Type: text/plain; charset=UTF-8
Content-Length: 13

403 Forbidden
>>> print Request.blank('/', accept='text/html').get_response(ErrorResponse('forbidden'))
403 Forbidden
Content-Type: text/html; charset=UTF-8
Content-Length: 50

<title>403 Forbidden</title><h2>403 Forbidden</h2>

ErrorResponse supports adding a custom message for the user. For example:

return ErrorResponse(403, "You lack administrator privileges")

It also allows setting additional headers:

return ErrorResponse('auth', www_authenticate=('Digest', challenge))

WSGIHTTPException can do those things too, but in a rather clunky manner.

Anyway, here’s the implementation:

_ctype_text = 'text/plain'
_ctype_html = 'text/html'

_error_types = {
    'auth': 401,
    'forbidden': 403,
    'method': 405,
}

class ErrorResponse(object):
    def __init__(self, status, message=None, comment=None, **kw):
        if not isinstance(status, int):
            status = _error_types[status]
        self.status = status
        self.title = u"%s %s" % (status, status_reasons[status])
        self.message = unicode(message or u'')
        self.comment = unicode(comment or u'')
        self.kw = kw

    @wapp
    def __call__(self, req):
        ctype = req.accept.best_match([_ctype_text, _ctype_html], _ctype_text)
        if ctype == _ctype_text:
            return self.text_response
        else:
            return self.html_response

    @property
    def text_response(self):
        body = self.title
        if self.message:
            body += u'\n' + self.message
        if self.comment:
            body += u'\n\n' + self.comment
        return Response(body, self.status, ctype=_ctype_text, **self.kw)

    @property
    def html_response(self):
        title = escape_text(self.title)
        body = u"<title>%s</title><h2>%s</h2>%s" % (
            title, title,
            escape_text(self.message)
        )
        if self.comment:
            body += u'\n<!--\n%s\n-->' % escape_text(self.comment)
        return Response(body, self.status, ctype=_ctype_html, **self.kw)

For me this code replaced the 1000-lines, 40-classes monster that is webob.exc.

webob.exc is not going anywhere (backwards compatibility), but if you want try out this other approach, see pasteob.errors.