diff --git a/sanic/exceptions.py b/sanic/exceptions.py index e30c178c..a026575a 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -47,6 +47,10 @@ TRACEBACK_STYLE = ''' padding: 5px 10px; } + .tb-border { + padding-top: 20px; + } + .frame-descriptor { background-color: #e2eafb; } @@ -63,12 +67,9 @@ TRACEBACK_WRAPPER_HTML = ''' {style} -

{exc_name}

-

{exc_value}

-
-

Traceback (most recent call last):

- {frame_html} -

+ {inner_html} +

+

{exc_name}: {exc_value} while handling path {path}

@@ -77,6 +78,24 @@ TRACEBACK_WRAPPER_HTML = ''' ''' +TRACEBACK_WRAPPER_INNER_HTML = ''' +

{exc_name}

+

{exc_value}

+
+

Traceback (most recent call last):

+ {frame_html} +
+''' + +TRACEBACK_BORDER = ''' +
+ + The above exception was the direct cause of the + following exception: + +
+''' + TRACEBACK_LINE_HTML = '''

diff --git a/sanic/handlers.py b/sanic/handlers.py index 64df2c2c..6a87fd5d 100644 --- a/sanic/handlers.py +++ b/sanic/handlers.py @@ -9,7 +9,9 @@ from sanic.exceptions import ( SanicException, TRACEBACK_LINE_HTML, TRACEBACK_STYLE, - TRACEBACK_WRAPPER_HTML) + TRACEBACK_WRAPPER_HTML, + TRACEBACK_WRAPPER_INNER_HTML, + TRACEBACK_BORDER) from sanic.log import log from sanic.response import text, html @@ -24,19 +26,31 @@ class ErrorHandler: self.cached_handlers = {} self.debug = False - def _render_traceback_html(self, exception, request): - exc_type, exc_value, tb = sys.exc_info() - frames = extract_tb(tb) + def _render_exception(self, exception): + frames = extract_tb(exception.__traceback__) frame_html = [] for frame in frames: frame_html.append(TRACEBACK_LINE_HTML.format(frame)) + return TRACEBACK_WRAPPER_INNER_HTML.format( + exc_name=exception.__class__.__name__, + exc_value=exception, + frame_html=''.join(frame_html)) + + def _render_traceback_html(self, exception, request): + exc_type, exc_value, tb = sys.exc_info() + exceptions = [] + + while exc_value: + exceptions.append(self._render_exception(exc_value)) + exc_value = exc_value.__cause__ + return TRACEBACK_WRAPPER_HTML.format( style=TRACEBACK_STYLE, - exc_name=exc_type.__name__, - exc_value=exc_value, - frame_html=''.join(frame_html), + exc_name=exception.__class__.__name__, + exc_value=exception, + inner_html=TRACEBACK_BORDER.join(reversed(exceptions)), path=request.path) def add(self, exception, handler): diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index da8a74e9..006c2cc4 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -35,6 +35,15 @@ def handler_5(request): raise CustomServerError('Custom server error') +@exception_handler_app.route('/6/') +def handler_6(request, arg): + try: + foo = 1 / arg + except Exception as e: + raise e from ValueError("{}".format(arg)) + return text(foo) + + @exception_handler_app.exception(NotFound, ServerError) def handler_exception(request, exception): return text("OK") @@ -84,6 +93,26 @@ def test_inherited_exception_handler(): assert response.status == 200 +def test_chained_exception_handler(): + request, response = exception_handler_app.test_client.get( + '/6/0', debug=True) + assert response.status == 500 + + soup = BeautifulSoup(response.body, 'html.parser') + html = str(soup) + + assert 'response = handler(request, *args, **kwargs)' in html + assert 'handler_6' in html + assert 'foo = 1 / arg' in html + assert 'ValueError' in html + assert 'The above exception was the direct cause' in html + + summary_text = " ".join(soup.select('.summary')[0].text.split()) + assert ( + "ZeroDivisionError: division by zero " + "while handling path /6/0") == summary_text + + def test_exception_handler_lookup(): class CustomError(Exception): pass