import logging from typing import Union from flask import Flask, Response, jsonify from werkzeug.exceptions import HTTPException from werkzeug.http import HTTP_STATUS_CODES from tofu_api.common import string_utils from tofu_api.common.exceptions import AppException from .exceptions import InternalServerError T_Response = Union[Response, tuple[Response, int]] class RestApiErrorHandler: """ Error handler class for REST API errors. """ # Dependencies logger: logging.Logger # Options debug_mode: bool = False # Lookup table for HTTP status codes to API error codes _http_to_api_error_codes: dict[int, str] def __init__(self, *, debug_mode: bool = False): self.logger = logging.getLogger(type(self).__name__) self.debug_mode = debug_mode # Generate lookup table for HTTP status codes self._http_to_api_error_codes = { http_status: string_utils.str_to_snake_case(name) for http_status, name in HTTP_STATUS_CODES.items() } def register_error_handlers(self, app: Flask) -> None: """ Registers error handlers for different types of exceptions to the app. """ app.register_error_handler(AppException, self.handle_app_exception) app.register_error_handler(HTTPException, self.handle_http_exception) app.register_error_handler(Exception, self.handle_generic_exception) @staticmethod def handle_app_exception(exception: AppException) -> T_Response: """ Handles exceptions of type `AppException` that were not handled by any more specific handler. """ return jsonify(exception.to_dict()), exception.status_code def handle_http_exception(self, exception: HTTPException) -> T_Response: """ Handles exceptions of type `HTTPException`, i.e. any werkzeug HTTP exceptions. """ if exception.code >= 500: self.logger.exception('HTTP exception with status code %s: %s', exception.code, type(exception).__name__) return jsonify({ 'code': self._http_to_api_error_codes.get(exception.code, 'unknown_http_error'), 'message': exception.description, }), exception.code def handle_generic_exception(self, exception: Exception) -> T_Response: """ Fallback handler for any exceptions not handled by any other handler. """ self.logger.exception('Uncaught exception: %s', type(exception).__name__) wrapped_exception = InternalServerError('There was an uncaught error on the server.', inner_exception=exception) return jsonify(wrapped_exception.to_dict(debug=self.debug_mode)), wrapped_exception.status_code