Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 19 additions & 1 deletion examples/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import Response
from starlette.routing import Route
from starlette.routing import Mount, Route, WebSocketRoute
from starlette.templating import Jinja2Templates

from starception import install_error_handler
Expand Down Expand Up @@ -72,5 +72,23 @@ def css_view(request: Request) -> Response:
Route('/css', css_view),
Route('/template', template_view),
Route('/javascript', javascript_view),
Mount(
'/app',
routes=[
Route('/node', index_view, name='index1'),
Mount(
'/nested',
routes=[
Route('/node/{id:int}', index_view, name='nested_node'),
Route('/node/{id:uuid}-{user:str}-{counter:int}', index_view, name='params_node'),
Route('/node2', index_view, name='nested_node'),
Mount('/level2', routes=[Route('/node/{id}', index_view)]),
],
name='nested',
),
],
),
Mount('/subapp', app=Starlette(), name='subapp'),
WebSocketRoute('/ws', index_view),
],
)
75 changes: 74 additions & 1 deletion starception/exception_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.requests import Request
from starlette.responses import HTMLResponse, PlainTextResponse, Response
from starlette.routing import BaseRoute, Mount, NoMatchFound, Router
from starlette.types import Receive, Scope, Send
from urllib.parse import quote_plus

_editor: str = 'none'
Expand All @@ -24,6 +26,53 @@
}


@dataclasses.dataclass
class RouteInfo:
methods: typing.List[str]
type: str
middleware: typing.List[str]
path_name: str
path_pattern: str
endpoint: typing.Any

@property
def endpoint_name(self) -> str:
module_name = getattr(self.endpoint, '__module__')
base_name = getattr(self.endpoint, '__name__', getattr(self.endpoint.__class__, '__name__'))
return f'{module_name}.{base_name}'


def collect_routes(routes: typing.Iterable[BaseRoute], prefix: str = '') -> typing.Iterable[RouteInfo]:
for route in routes:
if isinstance(route, Mount) and len(route.routes):
yield from collect_routes(list(route.routes), route.path_format)
else:
endpoint = getattr(route, 'endpoint', getattr(route, 'app'))
yield RouteInfo(
methods=getattr(route, 'methods', []) or [],
type=type(route).__name__,
path_name=getattr(route, 'name', ''),
path_pattern=prefix + getattr(route, 'path_format', ''),
endpoint=endpoint,
middleware=[],
)


def not_found_renderer(request: Request, exc: BaseException) -> str:
routes = list(collect_routes(request.app.routes))
return jinja.get_template('custom_exceptions/not_found.html').render(
{
'routes': routes,
}
)


ExceptionRenderer = typing.Callable[[Request, BaseException], str]
exception_renderers: typing.Dict[typing.Type[BaseException], ExceptionRenderer] = {
NoMatchFound: not_found_renderer,
}


def set_editor(name: str) -> None:
"""
Set code editor.
Expand Down Expand Up @@ -51,7 +100,10 @@ def to_ide_link(path: str, lineno: int) -> str:
return template.format(path=path, lineno=lineno)


def install_error_handler(editor: str = '') -> None:
def install_error_handler(
editor: str = '',
custom_exception_renderers: typing.Optional[typing.Dict[typing.Type[BaseException], ExceptionRenderer]] = None,
) -> None:
"""
Replace Starlette debug exception handler in-place.

Expand All @@ -60,10 +112,22 @@ def install_error_handler(editor: str = '') -> None:
"""
set_editor(editor)

if custom_exception_renderers:
exception_renderers.update(custom_exception_renderers)

def bound_handler(self: ServerErrorMiddleware, request: Request, exc: Exception) -> Response:
return exception_handler(request, exc)

original_handler = getattr(Router, 'not_found')

async def patched_not_found(self: Router, scope: Scope, receive: Receive, send: Send) -> None:
if 'app' in scope and scope['app'].debug:
raise NoMatchFound('', {})
else:
await original_handler(self, scope, receive, send)

setattr(ServerErrorMiddleware, 'debug_response', bound_handler)
setattr(Router, 'not_found', patched_not_found)


def get_relative_filename(path: str) -> str:
Expand Down Expand Up @@ -247,6 +311,14 @@ def generate_html(request: Request, exc: Exception, limit: int = 15) -> str:
)
cause = getattr(cause, '__cause__')

custom_exception_block = ''
exception_renderer = exception_renderers.get(exc.__class__)
if exception_renderer:
try:
custom_exception_block = exception_renderer(request, exc)
except Exception as ex:
custom_exception_block = f"Got exception while rendering exception details: {ex.__class__.__name__} {ex}."

template = jinja.get_template('index.html')
return template.render(
{
Expand Down Expand Up @@ -290,6 +362,7 @@ def generate_html(request: Request, exc: Exception, limit: int = 15) -> str:
"Paths": Markup("<br>".join(sys.path)),
},
'environment': os.environ,
'custom_exception_block': custom_exception_block,
'solution': getattr(exc, 'solution', None),
}
)
Expand Down
35 changes: 35 additions & 0 deletions starception/templates/custom_exceptions/not_found.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<strong>Available routes</strong>
<br/>
<br/>
<table style="width: 100%;" class="table">
<thead>
<tr>
<th style="text-align: right">Name</th>
<th>Methods</th>
<th>Path pattern</th>
<th>Endpoint</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{% for route in routes %}
<tr>
<td style="text-align: right">
{{ route.path_name }}
</td>
<td>
{{ route.methods|join(', ') }}
</td>
<td>
{{ route.path_pattern }}
</td>
<td>
{{ route.endpoint_name }}
</td>
<td>
{{ route.type }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
9 changes: 9 additions & 0 deletions starception/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@
</div>
</header>
{% endif %}

{% if loop.index0 == 0 and custom_exception_block %}
<div style="padding: 20px 40px 0px 40px;">
<div style="background: var(--snippet-bg); padding: 24px;">
{{ custom_exception_block }}
</div>
</div>
{% endif %}

<div data-trace-target {% if loop.index0 > 0 %} class="collapsed"{% endif %}>
{% if loop.index0 > 0 %}
{% if stack_item.solution %}
Expand Down
17 changes: 17 additions & 0 deletions starception/templates/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,20 @@ dd {
.search-links a:hover {
text-decoration: underline;
}

.table {
width: 100%;
}

.table th, .table td {
text-align: left;
padding: 3px 8px;
}

.table tbody tr td {
border-top: 1px solid var(--data-list-border-color);
}

.table tbody tr:hover {
background-color: var(--data-list-bg-hover);
}