From 9838af83ca21f466328473b58bc370af9e4539e0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 18 Jul 2018 13:04:14 +0100 Subject: [PATCH] Add DebugMiddleware --- README.md | 21 +++++++++++++++++++++ starlette/debug.py | 42 ++++++++++++++++++++++++++++++++++++++++++ tests/test_debug.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 starlette/debug.py create mode 100644 tests/test_debug.py diff --git a/README.md b/README.md index 02715ec5..5f308832 100644 --- a/README.md +++ b/README.md @@ -376,6 +376,27 @@ def test_app(): --- +## Debugging + +You can use Starlette's `DebugMiddleware` to display simple error tracebacks in the browser. + +```python +from starlette.debug import DebugMiddleware + + +class App: + def __init__(self, scope): + self.scope = scope + + async def __call__(self, receive, send): + raise RuntimeError('Something went wrong') + + +app = DebugMiddleware(App) +``` + +--- + ## Decorators The `asgi_application` decorator takes a request/response function and turns diff --git a/starlette/debug.py b/starlette/debug.py new file mode 100644 index 00000000..b8d93340 --- /dev/null +++ b/starlette/debug.py @@ -0,0 +1,42 @@ +from starlette.datastructures import Headers +from starlette.response import HTMLResponse, PlainTextResponse +import html +import traceback + + +class DebugMiddleware: + def __init__(self, app): + self.app = app + + def __call__(self, scope): + return _DebugResponder(self.app, scope) + + +class _DebugResponder: + def __init__(self, app, scope): + self.scope = scope + self.asgi_instance = app(scope) + self.response_started = False + + async def __call__(self, receive, send): + self.raw_send = send + try: + await self.asgi_instance(receive, self.send) + except: + if self.response_started: + raise + headers = Headers(self.scope.get('headers', [])) + accept = headers.get('accept', '') + if 'text/html' in accept: + exc_html = html.escape(traceback.format_exc()) + content = f'

500 Server Error

{exc_html}
' + response = HTMLResponse(content, status_code=500) + else: + content = traceback.format_exc() + response = PlainTextResponse(content, status_code=500) + await response(receive, send) + + async def send(self, message): + if message['type'] == 'http.response.start': + self.response_started = True + await self.raw_send(message) diff --git a/tests/test_debug.py b/tests/test_debug.py new file mode 100644 index 00000000..4cff1120 --- /dev/null +++ b/tests/test_debug.py @@ -0,0 +1,42 @@ +from starlette import Request, Response, TestClient +from starlette.debug import DebugMiddleware +import pytest + + +def test_debug_text(): + def app(scope): + async def asgi(receive, send): + raise RuntimeError('Something went wrong') + return asgi + + client = TestClient(DebugMiddleware(app)) + response = client.get("/") + assert response.status_code == 500 + assert response.headers['content-type'].startswith('text/plain') + assert 'RuntimeError' in response.text + + +def test_debug_html(): + def app(scope): + async def asgi(receive, send): + raise RuntimeError('Something went wrong') + return asgi + + client = TestClient(DebugMiddleware(app)) + response = client.get("/", headers={'Accept': 'text/html, */*'}) + assert response.status_code == 500 + assert response.headers['content-type'].startswith('text/html') + assert 'RuntimeError' in response.text + + +def test_debug_after_response_sent(): + def app(scope): + async def asgi(receive, send): + response = Response(b'', status_code=204) + await response(receive, send) + raise RuntimeError('Something went wrong') + return asgi + + client = TestClient(DebugMiddleware(app)) + with pytest.raises(RuntimeError): + response = client.get("/")