When you quickstart a TG2 project, it will create an ErrorController for you in yourproject/controllers/error.py.
When an unhandled exception is raised in your application, and debugging is turned off, that exception goes all the way up the call stack, to the Error Middleware, which then tries to dispatch your ErrorController to display an error message to the client.
The idea is that you can set up error reporting, templates, whatever you like, for your ErrorController, to fully customize your error handling experience.
This usually works automagically, but it wasn’t working for me, so I decided to figure out how this whole process works.
Execution and Error Handling
When the result of tg.configuration.AppConfig.setup_tg_wsgi_app() is called, as part of the setup process, it wraps the application in error middleware, with a call to self.add_error_middleware().
First the application object is decorated by tg.error.ErrorHandler(), which will wrap it with an weberror.evalexception.EvalException object if debugging is enabled, or with a weberror.errormiddleware.ErrorMiddleware object if debugging is disabled.
The application is then decorated with a pylons.middleware.StatusCodeRedirect object, with a default path of "/error/document" and a list of handled error codes (403, 404) (defined in AppConfig.handle_status_codes). If debugging is disabled, the StatusCodeRedirect object is initialized with an extra error code—500.
Assuming debugging is disabled, the ErrorMiddleware decorator will catch any otherwise uncaught exceptions from the application.
StatusCodeRedirect.__call__() will call pylons.util.call_wsgi_application(), which will extract status code, headers, app_iter object, and exception info from the result of ErrorMiddleware.__call__().
If the resulting status code is in the list of handled error codes defined above, the StatusCodeRedirect object will start a second dispatch. This time, to the error controller. Or, more specifically, to whatever controller handles requests to the path defined above.
Additionally, the environment dictionary for the second dispatch will have the original request and response objects available as environ['pylons.original_response'] and environ['pylons.original_request'].
If there is no route to handle the "/error/document" url, or no path via object dispatch, the error will be passed through to paste, and handled by a very simple error document. These responses are defined in paste.httpexceptions.
Conclusion
As it turns out, my problem stemmed from the fact that I’d never updated my ErrorController to play nicely with Routes, so all error dispatches were failing and errors were being passed directly to paste. Oops! Here’s hoping this post saves one of you from similar anguish.
Any idea what happens to the original exception object?
Good question, Kyle. From what I can see, it gets swallowed by the middleware, and never makes it to your error controller.