Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nicer traceback formatting #2667

Merged
merged 68 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
6673acf
Establish basic file browser and index fallback
ahopkins Jan 24, 2023
fed2ef3
Remove location information
ahopkins Jan 24, 2023
36e3cc9
Use html5tagger for AutoIndex
ahopkins Jan 25, 2023
e8bb283
Valid HTML5
ahopkins Jan 25, 2023
39a4a75
Add new pages module
ahopkins Jan 26, 2023
2e36507
Refactor to allow for common pages
ahopkins Jan 26, 2023
ca0e933
No logging of exception
ahopkins Jan 26, 2023
2c8f180
squash
ahopkins Jan 26, 2023
fa6dbdd
Style fixes for file table
ahopkins Jan 26, 2023
10d4f28
Simple server to include autoindex
ahopkins Jan 26, 2023
d9c883e
Add a header bar with Sanic logo. Remove duplicate title element. Avo…
Tronic Jan 27, 2023
e328d44
Timestamps a bit less ugly.
Tronic Jan 27, 2023
41da8bb
Improve navigation with breadcrumbs
Tronic Jan 27, 2023
2038799
Remove parent directory link from table
Tronic Jan 27, 2023
859a813
Fix Sanic brand colour in breadcrumbs.
Tronic Jan 27, 2023
a00ec8a
Better UX for empty folders.
Tronic Jan 27, 2023
32d62c2
Style tweaks
Tronic Jan 27, 2023
b517523
URL sanitation.
Tronic Jan 27, 2023
faf1ff8
Fix the document title (needs positional argument).
Tronic Jan 27, 2023
77bdfa1
HTML error formatting rebuilt on top of BasePage, and using external …
Tronic Jan 27, 2023
ea09906
Refactored to sanic.pages.error module. Traceback and style tuning. P…
Tronic Jan 27, 2023
f30f53f
Move DirectoryHandler to app instance
ahopkins Jan 28, 2023
ae757c8
Merge branch 'issue2661' into niceback-error-handling
Tronic Jan 28, 2023
4f000ab
Define colors for light/default mode as well.
Tronic Jan 28, 2023
b46b81d
Set styles
ahopkins Jan 31, 2023
713abe3
Merge branch 'issue2661' of github.com:sanic-org/sanic into issue2661
ahopkins Jan 31, 2023
1b43aa5
No auto registration of fallback error handlers
ahopkins Jan 31, 2023
ddf3a49
Remove accidental file
ahopkins Jan 31, 2023
5dfd48f
Merge branch 'main' of github.com:sanic-org/sanic into issue2661
ahopkins Jan 31, 2023
2df5b19
Sans serif w/ autoindex monospace
ahopkins Jan 31, 2023
ff47448
Merge branch 'issue2661' into niceback-error-handling
Tronic Feb 2, 2023
ebc2f46
Minor style changes, loading TraceRite style and css properly, printi…
Tronic Feb 4, 2023
53b7a5a
Merge branch 'main' into niceback-error-handling
Tronic Feb 5, 2023
f4792a2
CSS fixes, incl. scaling layout for small screens.
Tronic Feb 5, 2023
5c65118
CSS files now start with comments, none inserted in code.
Tronic Feb 5, 2023
207f8af
setup.py
Tronic Feb 5, 2023
a6ff13c
Fallback for route name when no route found.
Tronic Feb 5, 2023
d6f2613
Merge branch 'main' of github.com:sanic-org/sanic into niceback-error…
ahopkins Feb 5, 2023
cf76c05
CSS loading with STYLE_APPEND
Tronic Feb 5, 2023
783a29b
Require tracerite=>1.0.0
Tronic Feb 5, 2023
7491d56
Merge branch 'niceback-error-handling' of github.com:sanic-org/sanic …
ahopkins Feb 5, 2023
47b2459
Suggestions
ahopkins Feb 6, 2023
a773ad2
Cleanup styles and add Sanic palette
ahopkins Feb 6, 2023
68bf26d
Merge branch 'main' into niceback-error-handling
Tronic Feb 6, 2023
3ddbda6
Merge branch 'html-error-handling-suggestions' into niceback-error-ha…
Tronic Feb 6, 2023
39b98e6
Logo shadow to make it show up on white background.
Tronic Feb 6, 2023
12ba685
Some Sanic branding to error page (#2673)
ahopkins Feb 6, 2023
526115c
Fix SVG: alt to desc, whitespace removed
Tronic Feb 6, 2023
da9ff33
Add traceback suppressions to commonly shown internals.
Tronic Feb 7, 2023
a6efebd
Cleanup.
Tronic Feb 7, 2023
ce19908
Implement key-value-table as dl instead of table element.
Tronic Feb 7, 2023
a5f732c
Style and layout changes, fix document title.
Tronic Feb 7, 2023
7e617c1
Misc styling
Tronic Feb 7, 2023
37f3607
Friendlify the error page.
Tronic Feb 8, 2023
4c55051
Fix scrollbar layout problems.
Tronic Feb 8, 2023
c328a3c
Color tweaks for Error Handling. (#2676)
smlbiobot Mar 1, 2023
1332e61
Merge branch 'main' into niceback-error-handling
Tronic Mar 1, 2023
cf239a0
Use exc.quiet to determine whether details should be open by default
Tronic Mar 6, 2023
114456b
Make full() and minimal() identical.
Tronic Mar 6, 2023
f3cd637
Remove old error page formatting.
Tronic Mar 6, 2023
2be6376
Cleanup
Tronic Mar 6, 2023
35aaa62
Update tests for new error pages
Tronic Mar 6, 2023
384c963
isort
Tronic Mar 6, 2023
fc5c2df
Typing
Tronic Mar 6, 2023
1cf23f2
Suppress mypy for tracerite while it doesn't have typing stub
Tronic Mar 6, 2023
8b0ffbb
Change requests
Tronic Mar 6, 2023
ee290ba
Sanic pages coloring (#2703)
ahopkins Mar 6, 2023
ff0f3e6
Merge branch 'main' into niceback-error-handling
Tronic Mar 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ module = [
"sanic_routing.*",
"aioquic.*",
"html5tagger.*",
"tracerite.*",
]
ignore_missing_imports = true
2 changes: 2 additions & 0 deletions sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,8 @@ async def handle_request(self, request: Request): # no cov
:param request: HTTP Request object
:return: Nothing
"""
__tracebackhide__ = True
ahopkins marked this conversation as resolved.
Show resolved Hide resolved

await self.dispatch(
"http.lifecycle.handle",
inline=True,
Expand Down
2 changes: 1 addition & 1 deletion sanic/application/logo.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

""" # noqa

SVG_LOGO = """<svg id=logo alt=Sanic viewBox="0 0 964 279"><path d="M107 222c9-2 10-20 1-22s-20-2-30-2-17 7-16 14 6 10 15 10h30zm115-1c16-2 30-11 35-23s6-24 2-33-6-14-15-20-24-11-38-10c-7 3-10 13-5 19s17-1 24 4 15 14 13 24-5 15-14 18-50 0-74 0h-17c-6 4-10 15-4 20s16 2 23 3zM251 83q9-1 9-7 0-15-10-16h-13c-10 6-10 20 0 22zM147 60c-4 0-10 3-11 11s5 13 10 12 42 0 67 0c5-3 7-10 6-15s-4-8-9-8zm-33 1c-8 0-16 0-24 3s-20 10-25 20-6 24-4 36 15 22 26 27 78 8 94 3c4-4 4-12 0-18s-69 8-93-10c-8-7-9-23 0-30s12-10 20-10 12 2 16-3 1-15-5-18z" fill="#ff0d68"/><path d="M676 74c0-14-18-9-20 0s0 30 0 39 20 9 20 2zm-297-10c-12 2-15 12-23 23l-41 58H340l22-30c8-12 23-13 30-4s20 24 24 38-10 10-17 10l-68 2q-17 1-48 30c-7 6-10 20 0 24s15-8 20-13 20 -20 58-21h50 c20 2 33 9 52 30 8 10 24-4 16-13L384 65q-3-2-5-1zm131 0c-10 1-12 12-11 20v96c1 10-3 23 5 32s20-5 17-15c0-23-3-46 2-67 5-12 22-14 32-5l103 87c7 5 19 1 18-9v-64c-3-10-20-9-21 2s-20 22-30 13l-97-80c-5-4-10-10-18-10zM701 76v128c2 10 15 12 20 4s0-102 0-124s-20-18-20-7z M850 63c-35 0-69-2-86 15s-20 60-13 66 13 8 16 0 1-10 1-27 12-26 20-32 66-5 85-5 31 4 31-10-18-7-54-7M764 159c-6-2-15-2-16 12s19 37 33 43 23 8 25-4-4-11-11-14q-9-3-22-18c-4-7-3-16-10-19zM828 196c-4 0-8 1-10 5s-4 12 0 15 8 2 12 2h60c5 0 10-2 12-6 3-7-1-16-8-16z" fill="#e1e1e1"/></svg>""" # noqa
SVG_LOGO_SIMPLE = """<svg id=logo-simple viewBox="0 0 964 279"><desc>Sanic</desc><path d="M107 222c9-2 10-20 1-22s-20-2-30-2-17 7-16 14 6 10 15 10h30zm115-1c16-2 30-11 35-23s6-24 2-33-6-14-15-20-24-11-38-10c-7 3-10 13-5 19s17-1 24 4 15 14 13 24-5 15-14 18-50 0-74 0h-17c-6 4-10 15-4 20s16 2 23 3zM251 83q9-1 9-7 0-15-10-16h-13c-10 6-10 20 0 22zM147 60c-4 0-10 3-11 11s5 13 10 12 42 0 67 0c5-3 7-10 6-15s-4-8-9-8zm-33 1c-8 0-16 0-24 3s-20 10-25 20-6 24-4 36 15 22 26 27 78 8 94 3c4-4 4-12 0-18s-69 8-93-10c-8-7-9-23 0-30s12-10 20-10 12 2 16-3 1-15-5-18z" fill="#ff0d68"/><path d="M676 74c0-14-18-9-20 0s0 30 0 39 20 9 20 2zm-297-10c-12 2-15 12-23 23l-41 58H340l22-30c8-12 23-13 30-4s20 24 24 38-10 10-17 10l-68 2q-17 1-48 30c-7 6-10 20 0 24s15-8 20-13 20 -20 58-21h50 c20 2 33 9 52 30 8 10 24-4 16-13L384 65q-3-2-5-1zm131 0c-10 1-12 12-11 20v96c1 10-3 23 5 32s20-5 17-15c0-23-3-46 2-67 5-12 22-14 32-5l103 87c7 5 19 1 18-9v-64c-3-10-20-9-21 2s-20 22-30 13l-97-80c-5-4-10-10-18-10zM701 76v128c2 10 15 12 20 4s0-102 0-124s-20-18-20-7z M850 63c-35 0-69-2-86 15s-20 60-13 66 13 8 16 0 1-10 1-27 12-26 20-32 66-5 85-5 31 4 31-10-18-7-54-7M764 159c-6-2-15-2-16 12s19 37 33 43 23 8 25-4-4-11-11-14q-9-3-22-18c-4-7-3-16-10-19zM828 196c-4 0-8 1-10 5s-4 12 0 15 8 2 12 2h60c5 0 10-2 12-6 3-7-1-16-8-16z" fill="#1f1f1f"/></svg>""" # noqa

ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")

Expand Down
140 changes: 12 additions & 128 deletions sanic/errorpages.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from sanic.exceptions import BadRequest, SanicException
from sanic.helpers import STATUS_CODES
from sanic.log import deprecation, logger
from sanic.pages.error import ErrorPage
from sanic.response import html, json, text


Expand All @@ -38,10 +39,9 @@
from sanic import HTTPResponse, Request

DEFAULT_FORMAT = "auto"
FALLBACK_TEXT = (
"The server encountered an internal error and "
"cannot complete your request."
)
FALLBACK_TEXT = """\
The application encountered an unexpected error and could not continue.\
"""
FALLBACK_STATUS = 500
JSON = "application/json"

Expand Down Expand Up @@ -117,134 +117,18 @@ class HTMLRenderer(BaseRenderer):
The default fallback type.
"""

TRACEBACK_STYLE = """
html { font-family: sans-serif }
h2 { color: #888; }
.tb-wrapper p, dl, dd { margin: 0 }
.frame-border { margin: 1rem }
.frame-line > *, dt, dd { padding: 0.3rem 0.6rem }
.frame-line, dl { margin-bottom: 0.3rem }
.frame-code, dd { font-size: 16px; padding-left: 4ch }
.tb-wrapper, dl { border: 1px solid #eee }
.tb-header,.obj-header {
background: #eee; padding: 0.3rem; font-weight: bold
}
.frame-descriptor, dt { background: #e2eafb; font-size: 14px }
"""
TRACEBACK_WRAPPER_HTML = (
"<div class=tb-header>{exc_name}: {exc_value}</div>"
"<div class=tb-wrapper>{frame_html}</div>"
)
TRACEBACK_BORDER = (
"<div class=frame-border>"
"The above exception was the direct cause of the following exception:"
"</div>"
)
TRACEBACK_LINE_HTML = (
"<div class=frame-line>"
"<p class=frame-descriptor>"
"File {0.filename}, line <i>{0.lineno}</i>, "
"in <code><b>{0.name}</b></code>"
"<p class=frame-code><code>{0.line}</code>"
"</div>"
)
OBJECT_WRAPPER_HTML = (
"<div class=obj-header>{title}</div>"
"<dl class={obj_type}>{display_html}</dl>"
)
OBJECT_DISPLAY_HTML = "<dt>{key}</dt><dd><code>{value}</code></dd>"
OUTPUT_HTML = (
"<!DOCTYPE html><html lang=en>"
"<meta charset=UTF-8><title>{title}</title>\n"
"<style>{style}</style>\n"
"<h1>{title}</h1><p>{text}\n"
"{body}"
)

def full(self) -> HTTPResponse:
return html(
self.OUTPUT_HTML.format(
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body=self._generate_body(full=True),
),
status=self.status,
page = ErrorPage(
debug=self.debug,
title=super().title,
text=super().text,
request=self.request,
exc=self.exception,
)
return html(page.render(), status=self.status, headers=self.headers)

def minimal(self) -> HTTPResponse:
return html(
self.OUTPUT_HTML.format(
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body=self._generate_body(full=False),
),
status=self.status,
headers=self.headers,
)

@property
def text(self):
return escape(super().text)

@property
def title(self):
return escape(f"⚠️ {super().title}")

def _generate_body(self, *, full):
lines = []
if full:
_, exc_value, __ = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__

traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
appname = escape(self.request.app.name)
name = escape(self.exception.__class__.__name__)
value = escape(self.exception)
path = escape(self.request.path)
lines += [
f"<h2>Traceback of {appname} " "(most recent call last):</h2>",
f"{traceback_html}",
"<div class=summary><p>",
f"<b>{name}: {value}</b> "
f"while handling path <code>{path}</code>",
"</div>",
]

for attr, display in (("context", True), ("extra", bool(full))):
info = getattr(self.exception, attr, None)
if info and display:
lines.append(self._generate_object_display(info, attr))

return "\n".join(lines)

def _generate_object_display(
self, obj: t.Dict[str, t.Any], descriptor: str
) -> str:
display = "".join(
self.OBJECT_DISPLAY_HTML.format(key=key, value=value)
for key, value in obj.items()
)
return self.OBJECT_WRAPPER_HTML.format(
title=descriptor.title(),
display_html=display,
obj_type=descriptor.lower(),
)

def _format_exc(self, exc):
frames = extract_tb(exc.__traceback__)
frame_html = "".join(
self.TRACEBACK_LINE_HTML.format(frame) for frame in frames
)
return self.TRACEBACK_WRAPPER_HTML.format(
exc_name=escape(exc.__class__.__name__),
exc_value=escape(exc),
frame_html=frame_html,
)
return self.full()


class TextRenderer(BaseRenderer):
Expand Down
31 changes: 25 additions & 6 deletions sanic/pages/base.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
from abc import ABC, abstractmethod

from html5tagger import HTML, Document
from html5tagger import HTML, Builder, Document

from sanic import __version__ as VERSION
from sanic.application.logo import SVG_LOGO
from sanic.application.logo import SVG_LOGO_SIMPLE
from sanic.pages.css import CSS


class BasePage(ABC, metaclass=CSS): # no cov
TITLE = "Unknown"
TITLE = "Sanic"
HEADING = None
CSS: str
doc: Builder

def __init__(self, debug: bool = True) -> None:
self.doc = Document(self.TITLE, lang="en")
self.debug = debug

@property
def style(self) -> str:
return self.CSS

def render(self) -> str:
self.doc = Document(self.TITLE, lang="en", id="sanic")
self._head()
self._body()
self._foot()
Expand All @@ -28,7 +30,7 @@ def render(self) -> str:
def _head(self) -> None:
self.doc.style(HTML(self.style))
with self.doc.header:
self.doc.div(self.TITLE)
self.doc.div(self.HEADING or self.TITLE)

def _foot(self) -> None:
with self.doc.footer:
Expand All @@ -37,14 +39,31 @@ def _foot(self) -> None:
self._sanic_logo()
if self.debug:
self.doc.div(f"Version {VERSION}")
with self.doc.div:
for idx, (title, href) in enumerate(
(
("Docs", "https://sanic.dev"),
("Help", "https://sanic.dev/en/help.html"),
("GitHub", "https://github.com/sanic-org/sanic"),
)
):
if idx > 0:
self.doc(" | ")
self.doc.a(
title,
href=href,
target="_blank",
referrerpolicy="no-referrer",
)
self.doc.div("DEBUG mode")

@abstractmethod
def _body(self) -> None:
...

def _sanic_logo(self) -> None:
self.doc.a(
HTML(SVG_LOGO),
HTML(SVG_LOGO_SIMPLE),
href="https://sanic.dev",
target="_blank",
referrerpolicy="no-referrer",
Expand Down
4 changes: 2 additions & 2 deletions sanic/pages/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ class CSS(ABCMeta):
def __new__(cls, name, bases, attrs):
Page = super().__new__(cls, name, bases, attrs)
# Use a locally defined STYLE or the one from styles directory
s = _extract_style(attrs.get("STYLE"), name)
Page.STYLE = f"\n/* {name} */\n{s.strip()}\n" if s else ""
Page.STYLE = _extract_style(attrs.get("STYLE_FILE"), name)
Page.STYLE += attrs.get("STYLE_APPEND", "")
# Combine with all ancestor styles
Page.CSS = "".join(
Class.STYLE
Expand Down
109 changes: 109 additions & 0 deletions sanic/pages/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from typing import Any, Mapping

import tracerite.html

from html5tagger import E
from tracerite import html_traceback, inspector

from sanic.request import Request

from .base import BasePage


# Avoid showing the request in the traceback variable inspectors
inspector.blacklist_types += (Request,)
Tronic marked this conversation as resolved.
Show resolved Hide resolved

ENDUSER_TEXT = """\
We're sorry, but it looks like something went wrong. Please try refreshing \
the page or navigating back to the homepage. If the issue persists, our \
technical team is working to resolve it as soon as possible. We apologize \
for the inconvenience and appreciate your patience.\
"""


class ErrorPage(BasePage):
STYLE_APPEND = tracerite.html.style

def __init__(
self,
debug: bool,
title: str,
text: str,
request: Request,
exc: Exception,
) -> None:
super().__init__(debug)
name = request.app.name.replace("_", " ").strip()
if name.islower():
name = name.title()
ahopkins marked this conversation as resolved.
Show resolved Hide resolved
self.TITLE = f"Application {name} cannot handle your request"
self.HEADING = E("Application ").strong(name)(
" cannot handle your request"
)
self.title = title
self.text = text
self.request = request
self.exc = exc
self.details_open = not getattr(exc, "quiet", False)

def _head(self) -> None:
self.doc._script(tracerite.html.javascript)
super()._head()

def _body(self) -> None:
debug = self.request.app.debug
route_name = self.request.name or "[route not found]"
with self.doc.main:
self.doc.h1(f"⚠️ {self.title}").p(self.text)
# Show context details if available on the exception
context = getattr(self.exc, "context", None)
if context:
self._key_value_table(
"Issue context", "exception-context", context
)

if not debug:
with self.doc.div(id="enduser"):
self.doc.p(ENDUSER_TEXT).p.a("Front Page", href="/")
return
# Show additional details in debug mode,
# open by default for 500 errors
with self.doc.details(open=self.details_open, class_="smalltext"):
# Show extra details if available on the exception
extra = getattr(self.exc, "extra", None)
if extra:
self._key_value_table(
"Issue extra data", "exception-extra", extra
)

self.doc.summary(
"Details for developers (Sanic debug mode only)"
)
if self.exc:
with self.doc.div(class_="exception-wrapper"):
self.doc.h2(f"Exception in {route_name}:")
self.doc(
html_traceback(self.exc, include_js_css=False)
)

self._key_value_table(
f"{self.request.method} {self.request.path}",
"request-headers",
self.request.headers,
)

def _key_value_table(
self, title: str, table_id: str, data: Mapping[str, Any]
) -> None:
with self.doc.div(class_="key-value-display"):
self.doc.h2(title)
with self.doc.dl(id=table_id, class_="key-value-table smalltext"):
for key, value in data.items():
# Reading values may cause a new exception, so suppress it
try:
value = str(value)
except Exception:
value = E.em("Unable to display value")
self.doc.dt.span(key, class_="nobr key").span(": ").dd(
value
)
Loading