123 lines
4.1 KiB
Python
123 lines
4.1 KiB
Python
"""Custom error handler and error page rendering endpoint"""
|
|
import enum
|
|
import logging
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
from dataclasses import field
|
|
from typing import Tuple
|
|
|
|
import flask
|
|
import werkzeug
|
|
|
|
|
|
class ErrorColor(enum.Enum):
|
|
"""Enum of possible colors for the error page to be rendered with
|
|
|
|
Colors can be used to indicate the severity of the problem to a user. Generally, red is reserved
|
|
for fatal errors (errors that the user cannot fix themselves), orange for user input or
|
|
temporary errors (errors that the user can potentially fix themselves), and blue for
|
|
informational errors that don't necessarily indicate a problem, but still needs to block user
|
|
access.
|
|
|
|
Names are the intended usage of the color while values are the CSS classes that the page
|
|
uses for the corresponding color.
|
|
"""
|
|
|
|
WARNING = "warning"
|
|
ERROR = "error"
|
|
INFORMATIONAL = "informational"
|
|
|
|
|
|
class ErrorIcon(enum.Enum):
|
|
"""Enum of possible icons for the error page
|
|
|
|
Names are the intended usage of the icon while values are the Font Awesome CSS class structure
|
|
for the corresponding icon.
|
|
"""
|
|
|
|
EXCLAMATION = "fas fa-exclamation-triangle"
|
|
INFORMATION = "fas fa-exclamation-circle"
|
|
|
|
|
|
@dataclass
|
|
class ErrorDetails:
|
|
"""Container of details for an error that can be used to render an error page
|
|
|
|
:param code: The HTTP response code for the error
|
|
:param title: Page title/short description of the error
|
|
:param description: Long description of the error. Ideally should include an action the user
|
|
can take to debug or fix the problem.
|
|
:param icon: The icon that should be added to the page
|
|
:param color: The color of the page highlights
|
|
"""
|
|
|
|
code: int = 500
|
|
title: str = "Internal Server Error"
|
|
description: str = "The application encountered an unhandled error while trying to fulfill the request."
|
|
icon: ErrorIcon = ErrorIcon.EXCLAMATION
|
|
color: ErrorColor = ErrorColor.ERROR
|
|
event_id: str = field(default_factory=lambda: uuid.uuid4().hex)
|
|
|
|
|
|
def handle_error(error: Exception) -> Tuple[str, int]:
|
|
"""Handle an exception raised elsewhere in the application
|
|
|
|
:returns: Rendered template of an error page for the raised exception
|
|
"""
|
|
logger = logging.getLogger(__name__)
|
|
# If the application is running in debug mode then we re-raise the exception so that the
|
|
# developer gets the helpful browser-rendered stack trace werkzeug provides
|
|
if flask.current_app.config["DEBUG"]:
|
|
logger.warning(
|
|
"Application is running in debug mode, stack trace will be returned to client"
|
|
)
|
|
raise error
|
|
|
|
details = ErrorDetails()
|
|
|
|
if isinstance(error, NotImplementedError):
|
|
details.title = "Not Implemented"
|
|
details.description = "The functionality for this resource has not yet been implemented. The resource name is reserved for for future use"
|
|
details.color = ErrorColor.INFORMATIONAL
|
|
details.icon = ErrorIcon.INFORMATION
|
|
|
|
elif isinstance(error, werkzeug.exceptions.HTTPException):
|
|
details.code = error.code
|
|
details.title = error.name
|
|
details.description = error.description
|
|
if 400 <= error.code < 500:
|
|
details.color = ErrorColor.WARNING
|
|
details.icon = ErrorIcon.INFORMATION
|
|
|
|
else:
|
|
logger.exception(
|
|
f"Event ID {details.event_id}: unhandled application error at '{flask.request.full_path}'"
|
|
)
|
|
|
|
logger.error(
|
|
f"Event ID {details.event_id}: Error {details.code} {details.description}"
|
|
)
|
|
|
|
def _render_template(
|
|
title: str,
|
|
error: ErrorDetails,
|
|
) -> str:
|
|
"""Stub function to type, enumerate, and document the template parameters
|
|
|
|
:param title: Value of the page title header
|
|
:param error: Container of error details about the error to render on the page
|
|
"""
|
|
return flask.render_template(
|
|
"error.html.j2",
|
|
title=title,
|
|
error=error,
|
|
)
|
|
|
|
return (
|
|
_render_template(
|
|
title=f"{details.title} ({details.code})",
|
|
error=details,
|
|
),
|
|
details.code,
|
|
)
|