diff --git a/sprockets/http/mixins.py b/sprockets/http/mixins.py index 1be20b7..bc39ed5 100644 --- a/sprockets/http/mixins.py +++ b/sprockets/http/mixins.py @@ -8,6 +8,7 @@ HTTP related utility mixins. """ import logging import json +import traceback from tornado import httputil @@ -79,7 +80,7 @@ class ErrorWriter(object): Mix this class in to your inheritance chain to include error bodies as a standard JSON document. The error document has - two simple properties: + three simple properties: **type** This is the type of exception that occurred or ``null``. @@ -98,18 +99,27 @@ class ErrorWriter(object): neither an exception nor a custom reason, the string ``Unknown`` will be used. + **traceback** + If the application is configured to serve tracebacks and the + error was caused by an exception (based on ``exc_info`` kwarg), + then this is the formatted traceback as an array of strings + returned from :func:`traceback.format_exception`. Otherwise, + this property is set to ``null``. + """ def write_error(self, status_code, **kwargs): - error_body = {'type': None} + error_body = {'type': None, 'traceback': None} exc_type, exc_value, _ = kwargs.get('exc_info', (None, None, None)) if exc_type and exc_value: error_body['type'] = exc_type.__name__ error_body.setdefault('message', str(exc_value)) + if self.settings.get('serve_traceback', False): + error_body['traceback'] = traceback.format_exception( + *kwargs['exc_info']) else: - error_body.setdefault('message', - kwargs.get('reason', - _get_http_reason(status_code))) + reason = kwargs.get('reason', _get_http_reason(status_code)) + error_body.setdefault('message', reason) self.set_header('Content-Type', 'application/json') self.write(json.dumps(error_body).encode('utf-8')) diff --git a/tests.py b/tests.py index a6bdaca..33736d4 100644 --- a/tests.py +++ b/tests.py @@ -86,11 +86,22 @@ class ErrorLoggerTests(testing.AsyncHTTPTestCase): class ErrorWriterTests(testing.AsyncHTTPTestCase): + def setUp(self): + self._application = None + super(ErrorWriterTests, self).setUp() + + @property + def application(self): + if self._application is None: + self._application = web.Application([ + web.url(r'/status/(?P\d+)', + examples.StatusHandler), + web.url(r'/fail/(?P\d+)', RaisingHandler), + ]) + return self._application + def get_app(self): - return web.Application([ - web.url(r'/status/(?P\d+)', examples.StatusHandler), - web.url(r'/fail/(?P\d+)', RaisingHandler), - ]) + return self.application def _decode_response(self, response): content_type = response.headers['Content-Type'] @@ -141,3 +152,12 @@ class ErrorWriterTests(testing.AsyncHTTPTestCase): body = self._decode_response(response) self.assertEqual(body['message'], 'Unknown') + + def test_that_error_json_honors_serve_traceback(self): + self.application.settings['serve_traceback'] = True + + response = self.fetch('/fail/400') + self.assertEqual(response.code, 400) + + body = self._decode_response(response) + self.assertGreater(len(body['traceback']), 0)