Fix error handling

Previously, some kinds of errors would throw nice and fun cascades
of exceptions.

We now have a nice, clean error page for 500 and 503 (backend
connectivity) errors which includes minimal debugging information
for productive setups and a traceback for development setups.

In any case, the full exception is logged to the log with an error
ID which is printed on the error page.
This commit is contained in:
Jonas Schäfer
2021-01-21 15:33:34 +01:00
parent 065c065b3b
commit f363ff0b38
13 changed files with 308 additions and 112 deletions

View File

@@ -5,11 +5,16 @@ import os
import pathlib
import typing
import aiohttp
import quart.flask_patch
import quart
from quart import (
url_for,
render_template,
current_app,
redirect,
)
import environ
@@ -18,7 +23,7 @@ from . import colour, infra
from ._version import version, version_info # noqa:F401
def proc() -> typing.Dict[str, typing.Any]:
async def proc() -> typing.Dict[str, typing.Any]:
def url_for_avatar(entity: str, hash_: str,
**kwargs: typing.Any) -> str:
return url_for(
@@ -32,10 +37,16 @@ def proc() -> typing.Dict[str, typing.Any]:
**kwargs
)
try:
user_info = await infra.client.get_user_info()
except (aiohttp.ClientError, quart.exceptions.HTTPException):
user_info = {}
return {
"url_for_avatar": url_for_avatar,
"text_to_css": colour.text_to_css,
"lang": infra.selected_locale(),
"user_info": user_info,
}
@@ -45,6 +56,85 @@ def autosplit(s: typing.Union[str, typing.List[str]]) -> typing.List[str]:
return s
async def render_exception_template(
template: str,
exc: Exception,
error_id: str,
) -> str:
more: typing.Dict[str, str] = {}
if current_app.debug:
import traceback
more.update(
traceback="".join(traceback.format_exception(
type(exc),
exc,
exc.__traceback__,
)),
)
return await render_template(
template,
exception_short=str(
".".join([
type(exc).__module__,
type(exc).__qualname__,
]),
),
error_id=error_id,
**more,
)
async def backend_error_handler(exc: Exception) -> quart.Response:
error_id = infra.generate_error_id()
current_app.logger.error(
"error_id=%s returning 503 status page for exception",
error_id,
exc_info=exc,
)
return quart.Response(
await render_exception_template(
"backend_error.html",
exc,
error_id,
),
status=503,
)
async def generic_http_error(
exc: quart.exceptions.HTTPException,
) -> quart.Response:
return quart.Response(
await render_template(
"generic_http_error.html",
status=exc.status_code,
description=exc.description,
name=exc.name,
),
status=exc.status_code,
)
async def generic_error_handler(
exc: Exception,
) -> quart.Response:
error_id = infra.generate_error_id()
current_app.logger.error(
"error_id=%s returning 500 status page for exception",
error_id,
exc_info=exc,
)
return quart.Response(
await render_exception_template(
"internal_error.html",
exc,
error_id,
),
status=500,
)
@environ.config(prefix="SNIKKET_WEB")
class AppConfig:
secret_key = environ.var()
@@ -82,6 +172,25 @@ def create_app() -> quart.Quart:
app.config["AVATAR_CACHE_TTL"] = config.avatar_cache_ttl
app.context_processor(proc)
app.register_error_handler(
aiohttp.ClientConnectorError,
backend_error_handler, # type:ignore
)
app.register_error_handler(
quart.exceptions.HTTPException,
generic_http_error, # type:ignore
)
app.register_error_handler(
Exception,
generic_error_handler, # type:ignore
)
@app.route("/")
async def index() -> quart.Response:
if infra.client.has_session:
return redirect(url_for('user.index'))
return redirect(url_for('main.login'))
logging_config = app.config.get("LOGGING_CONFIG")
if logging_config is not None: