License Creative Commons Attribution Share Alike 3.0-US
Keywords
Custom errors (1) Error Handling (1) Pylons (10)
Permissions
Owner: Stou S.
Viewable by Everyone
Editable by All Siafoo Users
Hide
Know what you're getting – Unlike many sites, all our code is clearly licensed. Join Siafoo Now or Learn More

Custom HTTP Error messages in Pylons Atom Feed 0

Ever wondered how to get and display the HTTP exception text from within your Pylons 0.9.6 or 0.9.7 application?

1   The Why

On Siafoo if you try to access (in any way) a non existent article, or another type of object, you will get a 404 with a message that says "Article Not Found". This message is generated using a python call like:

# 's
1raise HTTPNotFound( _('%s Not Found') % item.type_name)

Originally we expected that the message passed to the exception would actually be available inside the error controller but later realized that it was not, undoubtedly a huge design flaw in Pylons or one of it's components. A solution (read hack) was needed!

Note

I really can't take any credit for the 0.9.6 solution (or the 0.9.7 for that matter), I tried at least two times to figure out how to get this to work and gave up both times. Eventually David figured out a good hack after spending ~6 hours on it. Since a new hack was needed for Siafoo's transiton to Pylons 0.9.7 and I spent several hours going down the wrong paths, before getting the solution from IRC [1], I've decided to write this up for posterity.

2   The Problem

Basically, there were two problems with the exception messages passing though:

  1. The HTTPException object is returning a WSGIResponse with a rendered html content, including a generic message and the detailed message--not just the detail message as we'd like since we have our own error handling anyway.
  2. The message inside the WSGIResponse returned by the HTTPException was kept around, but this message was not passed to the start_response function, which is responsible for calling the error mapper from our middleware.py. Instead, the default title of the error (ie NOT FOUND) was looked up in a table and passed as the status to the start_response function and eventually (through repl_start_response in the Pylons controller and check_status in Paste's StatusBasedForward) to our error_mapper. [2]

3   The Solution

3.1   Pylons 0.9.6

The most important piece is to actually define a module like this:

# 's
 1from paste.httpexceptions import *
2from paste.wsgiwrappers import WSGIResponse
3
4# From paste.httpexceptions
5def HTTPException_response(self, environ):
6 environ['siafoo.error_text'] = self.detail # this line added, and import moved to top of file
7 headers, content = self.prepare_content(environ)
8 resp = WSGIResponse(code=self.code, content=content)
9 resp.headers = resp.headers.fromlist(headers)
10 return resp
11
12# This should hopefully change all the subclasses as well... you could probably do some mojo with shuffling bases too.. but yikes.
13HTTPException.response = HTTPException_response

and always import your HTTP Exception objects from it.

Then create an error_mapper inside middleware.py:

# 's
 1def error_mapper(code, message, environ, global_conf=None, **kw):
2
3 if not global_conf:
4 global_conf = {}
5
6 if environ.get('pylons.error_call'):
7 return None
8 else:
9 environ['pylons.error_call'] = True
10
11 codes = [400, 401, 403, 404]
12
13 if not asbool(global_conf.get('debug')):
14 codes.append(500)
15
16 if environ.get('my.error_text'):
17 message = environ['my.error_text']
18
19 if code in codes:
20 # StatusBasedForward expects a relative URL (no SCRIPT_NAME)
21 return '/error/document/?%s' % (urllib.urlencode({'message': message,
22 'code': code,
23 'ur':'url':environ['PATH_INFO']}}))

and initialize it in the make_app call:

# 's
1if asbool(full_stack):
2
3 # Handle Python exceptions
4 app = ErrorHandler(app, global_conf, error_template=error_template,
5 **config['pylons.errorware'])
6
7 # Display error documents for 401, 403, 404 status codes (and 500 when
8 # debug is disabled)
9 app = ErrorDocuments(app, global_conf, mapper=error_mapper, **app_conf)

Finally setup the document action in the error.py controller:

# 's
1def document(self):
2 ''' Render the error document '''
3
4 c.err_code = request.params.get('code', '500')
5 c.err_message = request.params.get('message', '')
6 c.err_url = request.params.get('url', '')
7
8 render('error') # Render your custom template

3.2   Pylons 0.9.7

If you try to run the above hack on Pylons 0.9.7 you will get a DeprecationWarning like this:

.../config/middleware.py:172: DeprecationWarning: The 'error_template' errorware argument for customizing EvalException is deprecated, please remove it.
To customize EvalException's HTML, setup your own EvalException and ErrorMiddlewares instead of using ErrorHandler. **config['pylons.errorware'])

Because the error_mapper mechanism is missing, the message and url values need to be retrieved from the environ [3]. In this case the error.py:document action will look something like:

# 's
 1def document(self):
2 ''' Render the error document '''
3
4 resp = request.environ.get('pylons.original_response')
5
6 c.err_code = cgi.escape(request.GET.get('code', str(resp.status_int)))
7 c.err_message = request.environ.get('my.error_text', 'No Msg?!')
8 c.err_url = request.environ.get('my.error_url', '')
9
10 render('error') # Render your custom template

Also since Pylons 0.9.7 switched to WebException the patched exception object will not actually work and so there is no way to extract the exception message. The easiest way seems to be to sub-class the WSGIController and then to override either _perform_call or _inspect_call

Note

This solution will almost certainly work in 0.9.6, just make sure you copy/paste the correct WSGIController code

Warning

Use only ONE of the methods below, NOT both

3.2.1   _perform_call

Although this method is a lot cleaner it is theoretically slower than overriding _inspect_call because the exception is caught and then raised again. Note that you could call WSGIController._perform_call for even more clarity... but it will cost you an extra function call, every time.

# 's
 1from pylons.controllers import WSGIController
2
3class CustomWSGIController(WSGIController):
4 def _perform_call(self, func, args):
5 try:
6 __traceback_hide__ = 'before_and_this'
7 return func(**args)
8
9 # WSGIController._perform_call(self, func, args) # This will also work
10 except HTTPException, httpe:
11 print httpe
12 pylons.request.environ['my.error_text'] = httpe.message
13 pylons.request.environ['my.error_url'] = pylons.request.path_qs
14 raise

3.2.2   _inspect_call

This method might be theoretically faster (during error conditions) but its disadvantage is that in order to add three lines of custom code you must copy/paste a huge amount of foreign code that most certainly will change in the future. And come on you aren't writing Python for its blinding speed are you?

# 's
 1from pylons.controllers import WSGIController
2
3class CustomWSGIController(WSGIController):
4 def _inspect_call(self, func):
5 '''Calls a function with arguments from
6 :meth:`_get_method_args`
7
8 Given a function, inspect_call will inspect the function args
9 and call it with no further keyword args than it asked for.
10
11 If the function has been decorated, it is assumed that the
12 decorator preserved the function signature.
13
14 '''
15
16 # Check to see if the class has a cache of argspecs yet
17 try:
18 cached_argspecs = self.__class__._cached_argspecs
19 except AttributeError:
20 self.__class__._cached_argspecs = cached_argspecs = {}
21
22 try:
23 argspec = cached_argspecs[func.im_func]
24 except KeyError:
25 argspec = cached_argspecs[func.im_func] = inspect.getargspec(func)
26 kargs = self._get_method_args()
27
28 log_debug = self._pylons_log_debug
29 c_internal = self._py_object.c
30 args = None
31
32 if argspec[2]:
33 if self._py_object.config['pylons.c_attach_args']:
34 for k, val in kargs.iteritems():
35 setattr(c_internal, k, val)
36 args = kargs
37 else:
38 args = {}
39 argnames = argspec[0][isinstance(func, types.MethodType) and 1 or 0:]
40 for name in argnames:
41 if name in kargs:
42 if self._py_object.config['pylons.c_attach_args']:
43 setattr(c_internal, name, kargs[name])
44 args[name] = kargs[name]
45 if log_debug:
46 log.debug("Calling %r method with keyword args: **%r",
47 func.__name__, args)
48 try:
49 result = self._perform_call(func, args)
50 except HTTPException, httpe:
51 if log_debug:
52 log.debug("%r method raised HTTPException: %s (code: %s)",
53 func.__name__, httpe.__class__.__name__,
54 httpe.wsgi_response.code, exc_info=True)
55 result = httpe
56
57 pylons.request.environ['siafoo.error_text'] = httpe.message
58 pylons.request.environ['siafoo.error_url'] = pylons.request.path_qs
59
60 # 304 Not Modified's shouldn't have a content-type set
61 if result.wsgi_response.status_int == 304:
62 result.wsgi_response.headers.pop('Content-Type', None)
63 result._exception = True
64 except LegacyHTTPException, httpe:
65
66 print 'Legacy Exception?!?!'
67
68 if log_debug:
69 log.debug("%r method raised legacy HTTPException: %s (code: "
70 "%s)", func.__name__, httpe.__class__.__name__,
71 httpe.code, exc_info=True)
72 warnings.warn("Raising a paste.httpexceptions.HTTPException is "
73 "deprecated, use webob.exc.HTTPException instead",
74 DeprecationWarning, 2)
75 result = httpe.response(pylons.request.environ)
76 result.headers.pop('Content-Type')
77 result._exception = True
78
79 return result

4   Notes

[1]If it wasn't for benbangert in #pylons on FreeNode I would have given up on this.
[2]This is taken nearly verbatim from one of only 3 comments David has ever written inside the Siafoo code. This explanation may not be totally accurate for Pylons 0.9.7.
[3]There should actually be a better solutions for this but you'll have to write some custom middleware