tofu-api/tofu_api/common/rest/error_handler.py

74 lines
2.7 KiB
Python

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