"""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, )