From 0bc0bd0673f8c167558b62645cbba652f329ab08 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Mon, 29 Jan 2024 06:48:06 +0300 Subject: Create framework for error handling and handle connection errors --- gn2/wqflask/__init__.py | 9 ++++++++- gn2/wqflask/app_errors.py | 37 ++++++++++++++++++++++++++++++++++++- gn2/wqflask/external_errors.py | 18 ++++++++++++++++++ gn2/wqflask/oauth2/client.py | 27 ++++++++++++++++++--------- 4 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 gn2/wqflask/external_errors.py diff --git a/gn2/wqflask/__init__.py b/gn2/wqflask/__init__.py index bca95275..8cc2904c 100644 --- a/gn2/wqflask/__init__.py +++ b/gn2/wqflask/__init__.py @@ -51,13 +51,20 @@ app = Flask(__name__) app.config.from_object('gn2.default_settings') app.config.from_envvar('GN2_SETTINGS') +def numcoll(): + """Handle possible errors.""" + try: + return num_collections() + except Exception as _exc: + return "ERROR" + app.jinja_env.globals.update( undefined=jinja2.StrictUndefined, numify=formatting.numify, logged_in=user_logged_in, authserver_authorise_uri=authserver_authorise_uri, user_details=user_details, - num_collections=num_collections, + num_collections=numcoll, datetime=datetime) app.config["SESSION_REDIS"] = redis.from_url(app.config["REDIS_URL"]) diff --git a/gn2/wqflask/app_errors.py b/gn2/wqflask/app_errors.py index c6081e08..b0e3b665 100644 --- a/gn2/wqflask/app_errors.py +++ b/gn2/wqflask/app_errors.py @@ -1,9 +1,42 @@ """Handle errors at the application's top-level""" -from flask import flash, redirect, current_app, render_template +import os +import random +import datetime +import traceback + +from werkzeug.exceptions import InternalServerError from authlib.integrations.base_client.errors import InvalidTokenError +from flask import ( + flash, request, redirect, current_app, render_template, make_response) from gn2.wqflask.oauth2 import session from gn2.wqflask.decorators import AuthorisationError +from gn2.wqflask.external_errors import ExternalRequestError + +def render_error(exc): + """Render the errors consistently.""" + error_desc = str(exc) + time_str = datetime.datetime.utcnow().strftime('%l:%M%p UTC %b %d, %Y') + formatted_lines = f"{request.url} ({time_str}) \n{traceback.format_exc()}" + + current_app.logger.error("(error-id: %s): %s\n\n%s", + exc.errorid if hasattr(exc, "errorid") else uuid4(), + error_desc, + formatted_lines) + + animation = request.cookies.get(error_desc[:32]) + if not animation: + animation = random.choice([fn for fn in os.listdir( + "./gn2/wqflask/static/gif/error") if fn.endswith(".gif")]) + + resp = make_response(render_template( + "error.html", + message=error_desc, + stack={formatted_lines}, + error_image=animation, + version=current_app.config.get("GN_VERSION"))) + resp.set_cookie(error_desc[:32], animation) + return resp def handle_authorisation_error(exc: AuthorisationError): """Handle AuthorisationError if not handled anywhere else.""" @@ -20,6 +53,8 @@ def handle_invalid_token_error(exc: InvalidTokenError): __handlers__ = { AuthorisationError: handle_authorisation_error, + ExternalRequestError: lambda exc: render_error(exc), + InternalServerError: lambda exc: render_error(exc), InvalidTokenError: handle_invalid_token_error } diff --git a/gn2/wqflask/external_errors.py b/gn2/wqflask/external_errors.py new file mode 100644 index 00000000..c4e9a2c7 --- /dev/null +++ b/gn2/wqflask/external_errors.py @@ -0,0 +1,18 @@ +"""Errors caused by calls to external services.""" +import traceback +from uuid import uuid4 + +class ExternalRequestError(Exception): + """Raise when a request to an external endpoint fails.""" + + def __init__(self, + externaluri: str, + error: Exception): + """Initialise the error message.""" + self.errorid = uuid4() + self.error = error + self.extrainfo = extrainfo + super().__init__( + f"error-id: {self.errorid}: We got an error of type " + f"'{type(error).__name__}' trying to access {externaluri}:\n\n " + f"{''.join(traceback.format_exception(error))}") diff --git a/gn2/wqflask/oauth2/client.py b/gn2/wqflask/oauth2/client.py index c6a3110b..ed4dbbbf 100644 --- a/gn2/wqflask/oauth2/client.py +++ b/gn2/wqflask/oauth2/client.py @@ -1,4 +1,5 @@ """Common oauth2 client utilities.""" +import uuid import json import requests from typing import Any, Optional @@ -11,6 +12,7 @@ from authlib.integrations.requests_client import OAuth2Session from gn2.wqflask.oauth2 import session from gn2.wqflask.oauth2.checks import user_logged_in +from gn2.wqflask.external_errors import ExternalRequestError SCOPE = ("profile group role resource register-client user masquerade " "introspect migrate-data") @@ -76,10 +78,14 @@ def oauth2_post( def no_token_get(uri_path: str, **kwargs) -> Either: from gn2.utility.tools import AUTH_SERVER_URL - resp = requests.get(urljoin(AUTH_SERVER_URL, uri_path), **kwargs) - if resp.status_code == 200: - return Right(resp.json()) - return Left(resp) + uri = urljoin(AUTH_SERVER_URL, uri_path) + try: + resp = requests.get(uri, **kwargs) + if resp.status_code == 200: + return Right(resp.json()) + return Left(resp) + except requests.exceptions.RequestException as exc: + raise ExternalRequestError(uri, exc) from exc def no_token_post(uri_path: str, **kwargs) -> Either: from gn2.utility.tools import ( @@ -99,11 +105,14 @@ def no_token_post(uri_path: str, **kwargs) -> Either: }, ("data" if bool(data) else "json"): request_data } - resp = requests.post(urljoin(AUTH_SERVER_URL, uri_path), - **new_kwargs) - if resp.status_code == 200: - return Right(resp.json()) - return Left(resp) + try: + resp = requests.post(urljoin(AUTH_SERVER_URL, uri_path), + **new_kwargs) + if resp.status_code == 200: + return Right(resp.json()) + return Left(resp) + except requests.exceptions.RequestException as exc: + raise ExternalRequestError(uri, exc) from exc def post(uri_path: str, **kwargs) -> Either: """ -- cgit v1.2.3