diff --git a/requirements-dev.txt b/requirements-dev.txt index 1c34d695..8e0ffac1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,3 +12,4 @@ kyoukai falcon tornado aiofiles +beautifulsoup4 diff --git a/sanic/exceptions.py b/sanic/exceptions.py index b9e6bf00..430f1d29 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -1,6 +1,104 @@ -from .response import text +from .response import text, html from .log import log -from traceback import format_exc +from traceback import format_exc, extract_tb +import sys + +TRACEBACK_STYLE = ''' + +''' + +TRACEBACK_WRAPPER_HTML = ''' + +
+ {style} + + +{exc_value}
Traceback (most recent call last):
+ {frame_html} +
+ {exc_name}: {exc_value}
+ while handling uri {uri}
+
+ File {0.filename}, line {0.lineno},
+ in {0.name}
+
{0.line}
+ The server encountered an internal error and cannot complete + your request. +
+''' class SanicException(Exception): @@ -46,6 +144,21 @@ class Handler: self.handlers = {} self.sanic = sanic + def _render_traceback_html(self, exception, request): + exc_type, exc_value, tb = sys.exc_info() + frames = extract_tb(tb) + + frame_html = [] + for frame in frames: + frame_html.append(TRACEBACK_LINE_HTML.format(frame)) + + return TRACEBACK_WRAPPER_HTML.format( + style=TRACEBACK_STYLE, + exc_name=exc_type.__name__, + exc_value=exc_value, + frame_html=''.join(frame_html), + uri=request.url) + def add(self, exception, handler): self.handlers[exception] = handler @@ -77,11 +190,12 @@ class Handler: 'Error: {}'.format(exception), status=getattr(exception, 'status_code', 500)) elif self.sanic.debug: + html_output = self._render_traceback_html(exception, request) + response_message = ( 'Exception occurred while handling uri: "{}"\n{}'.format( request.url, format_exc())) log.error(response_message) - return text(response_message, status=500) + return html(html_output, status=500) else: - return text( - 'An error occurred while generating the response', status=500) + return html(INTERNAL_SERVER_ERROR_HTML, status=500) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 5cebfb87..a81e0d09 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,4 +1,5 @@ import pytest +from bs4 import BeautifulSoup from sanic import Sanic from sanic.response import text @@ -75,7 +76,13 @@ def test_handled_unhandled_exception(exception_app): request, response = sanic_endpoint_test( exception_app, uri='/divide_by_zero') assert response.status == 500 - assert response.body == b'An error occurred while generating the response' + soup = BeautifulSoup(response.body, 'html.parser') + assert soup.h1.text == 'Internal Server Error' + + message = " ".join(soup.p.text.split()) + assert message == ( + "The server encountered an internal error and " + "cannot complete your request.") def test_exception_in_exception_handler(exception_app): diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index 2e8bc359..c56713b6 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -2,6 +2,7 @@ from sanic import Sanic from sanic.response import text from sanic.exceptions import InvalidUsage, ServerError, NotFound from sanic.utils import sanic_endpoint_test +from bs4 import BeautifulSoup exception_handler_app = Sanic('test_exception_handler') @@ -21,6 +22,12 @@ def handler_3(request): raise NotFound("OK") +@exception_handler_app.route('/4') +def handler_4(request): + foo = bar + return text(foo) + + @exception_handler_app.exception(NotFound, ServerError) def handler_exception(request, exception): return text("OK") @@ -47,3 +54,20 @@ def test_text_exception__handler(): exception_handler_app, uri='/random') assert response.status == 200 assert response.text == 'OK' + + +def test_html_traceback_output_in_debug_mode(): + request, response = sanic_endpoint_test( + exception_handler_app, uri='/4', 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_4' in html + assert 'foo = bar' in html + + summary_text = " ".join(soup.select('.summary')[0].text.split()) + assert ( + "NameError: name 'bar' " + "is not defined while handling uri /4") == summary_text diff --git a/tox.ini b/tox.ini index a2f89206..009d971c 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ python = deps = aiohttp pytest + beautifulsoup4 commands = pytest tests {posargs}