angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Redirigir una excepción en Flask utilizando un decorator

Python el manejo de excepciones decorators son una poderosa forma de reducir el código try-except.

7 mayo 2022
En Flask
post main image
https://unsplash.com/@thiagojapyassu

En una aplicación Flask , normalmente se implementan manejadores de excepción globales. En muchos casos, esto es suficiente. ¿Pero qué pasa si quieres más control?

En un proyecto, me estaba conectando a un API y quería una serie de rutas que utilizaran el API para redirigir a una página de 'inicio' en caso de un error del API , con un mensaje apropiado, por supuesto. He implementado esto usando un manejador de excepciones 'redirect_decorator' que también tiene un parámetro que especifica el punto final. El decorator se utiliza para evitar tener que dar a cada ruta un código try-except.

Configuración del proyecto

Como siempre, crear un virtual environment, y directorios, por ejemplo, escribiendo:

Translated with www.DeepL.com/Translator (free version)
python3 -m venv flask_except
source flask_except/bin/activate
cd flask_except
mkdir project
cd project
mkdir app
pip install flask

En el directorio del proyecto se encuentra el archivo para ejecutar la aplicación:

# run.py
from app.factory import create_app

app = create_app()

if __name__ == '__main__':
    app.run(
        host='localhost',
        debug=True,
    )

Flask con un manejador de errores

Esto es más o menos una copia de lo que hay en la web Flask .

# factory.py
from flask import Flask

def internal_server_error(e):
    return 'An internal server error occurred', 500

def create_app():
    
    app = Flask(__name__)
    
    @app.route('/')
    def welcome():
        return 'Welcome'

    @app.route('/error')
    def error():
        a = 2/0
        return 'Error'

    app.register_error_handler(500, internal_server_error)

    return app

En el directorio del proyecto iniciamos la aplicación:

python run.py

Luego en el navegador puedes comprobar los endpoints:

http://127.0.0.1:5000
http://127.0.0.1:5000/error

Si quieres ver el mensaje de la función internal_server_error() entonces pon debug=False en run.py. En este caso, el servidor no se reinicia con los cambios.

Añadir un API que genere excepciones en caso de error

En Python, la mayoría de los paquetes externos generan excepciones cuando se producen errores. Eso es lo que vamos a simular aquí. Nuestro paquete API tiene una función y genera sus propias excepciones.

# api: custom exception
class ServiceAPIError(Exception):
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

# api: function
def service_api_function():
    if True:
        raise ServiceAPIError(
            reason='Connection problem',
            description='Remote system not responding',
        )

Si se produce un error, la función lanza un ServiceAPIError con un motivo y una descripción.

La redirección decorator

Para obtener la máxima flexibilidad quiero poder pasar un endpoint en el decorator. Esto significa envolver el decorator de nuevo.

# our redirect decorator
def redirect_on_service_api_error(endpoint):
    def decorator(f):
        @functools.wraps(f)
        def func(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except ServiceAPIError as e:
                return redirect(url_for(endpoint))
        return func
    return decorator

Añadir el API y redirigir el decorator

Nuestra aplicación tiene una o más rutas para ejecutar tareas. Cuando una tarea falla debido a un error API , queremos ser redirigidos a la ruta 'start'. He añadido declaraciones de impresión para ver toda la información disponible en nuestro decorator:

# factory.py (with api, redirect decorator)
from flask import Flask, redirect, url_for
import functools

# api: custom exception
class ServiceAPIError(Exception):
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

# api: function
def service_api_function():
    if True:
        raise ServiceAPIError(
            reason='Connection problem',
            description='Remote system not responding',
        )

# our redirect decorator
def redirect_on_service_api_error(endpoint):
    def decorator(f):
        @functools.wraps(f)
        def func(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except ServiceAPIError as e:
                # debugging
                print('Exception: {}, e = {}'.format(type(e).__name__, e))
                print('- function = {}, args = {}, kwargs = {}'.format(f.__name__, args, kwargs))
                print('- endpoint = {}'.format(endpoint))
                print('- e.args = {}, e.kwargs = {}'.format(e.args, e.kwargs))
                # redirect
                return redirect(url_for(endpoint))
        return func
    return decorator

def internal_server_error(e):
    return 'An internal server error occurred', 500

def create_app():
    
    app = Flask(__name__)

    @app.route('/')
    def welcome():
        return 'Welcome'

    @app.route('/error')
    def error():
        a = 2/0
        return 'Error'

    @app.route('/start')
    def start():
        return 'Start'

    @app.route('/tasks/<task>')
    @redirect_on_service_api_error('start')
    def tasks(task):
        print('tasks: task = {}'.format(task))
        service_api_function()
        return 'Started task'

    app.register_error_handler(500, internal_server_error)

    return app

Ahora apunte su navegador a:

http://127.0.0.1:5000/tasks/mytask

y observa que somos redirigidos a 'start'. Bien. En la consola se imprime lo siguiente:

Exception: ServiceAPIError, e = 
- function = tasks, args = (), kwargs = {'task': 'mytask'}
- endpoint = start
- e.args = (), e.kwargs = {'reason': 'Connection problem', 'description': 'Remote system not responding'}

Pero podemos hacerlo mejor: Mensaje intermitente

Cuando hay un error en API y somos redirigidos a 'start', también queremos mostrar lo que ha pasado. Podemos hacer esto usando la función de mensaje flash() en Flask. Antes de redirigir en nuestro decorator, flash() el mensaje:

flash("There was a problem in '{}' with task  '{}': {}".\
    format(f.__name__, kwargs['task'], e.kwargs['reason']))

No implementaré esto en las plantillas sino que utilizaré la función get_flashed_messages() en la ruta 'start':

    @app.route('/start')
    def start():
        return 'Start<br>' + ', '.join(get_flashed_messages())

Para usar flash() también debemos añadir el secret_key a nuestra aplicación.

Nuestra aplicación final:

# factory.py (with api, redirect decorator, flashed messages)
from flask import Flask, redirect, url_for, flash, get_flashed_messages
import functools

# api: custom exception
class ServiceAPIError(Exception):
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

# api: function
def service_api_function():
    if True:
        raise ServiceAPIError(
            reason='Connection problem',
            description='Remote system not responding',
        )

# our redirect decorator
def redirect_on_service_api_error(endpoint):
    def decorator(f):
        @functools.wraps(f)
        def func(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except ServiceAPIError as e:
                # debugging
                print('Exception: {}, e = {}'.format(type(e).__name__, e))
                print('- function = {}, args = {}, kwargs = {}'.format(f.__name__, args, kwargs))
                print('- endpoint = {}'.format(endpoint))
                print('- e.args = {}, e.kwargs = {}'.format(e.args, e.kwargs))
                # flash
                flash("There was a problem in '{}' with task  '{}': {}".\
                    format(f.__name__, kwargs['task'], e.kwargs['reason']))
                # redirect
                return redirect(url_for(endpoint))
        return func
    return decorator

def internal_server_error(e):
    return 'An internal server error occurred', 500

def create_app():
    
    app = Flask(__name__)

    app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'

    @app.route('/')
    def welcome():
        return 'Welcome'

    @app.route('/error')
    def error():
        a = 2/0
        return 'Error'

    @app.route('/start')
    def start():
        return 'Start<br>' + ', '.join(get_flashed_messages())

    @app.route('/tasks/<task>')
    @redirect_on_service_api_error('start')
    def tasks_task(task):
        print('tasks, task = {}'.format(task))
        service_api_function()
        return 'Started task'

    app.register_error_handler(500, internal_server_error)

    return app

Ahora apunta tu navegador a:

http://127.0.0.1:5000/tasks/mytask

y somos redirigidos a 'start', pero también se muestra el mensaje. ¡Ahora ya sabemos lo que ha pasado!

Start
There was a problem in 'tasks_task' with task 'mytask': Connection problem

Resumen

Comparado con try-except en cada ruta tenemos mucho menos código. Y en nuestro manejador de excepciones decorator tenemos toda la información que necesitamos para mostrar un mensaje decente.

Enlaces / créditos

Flask - Handling Application Errors
https://flask.palletsprojects.com/en/2.1.x/errorhandling

Flask - Message Flashing
https://flask.palletsprojects.com/en/2.1.x/patterns/flashing

How do I pass extra arguments to a Python decorator?
https://stackoverflow.com/questions/10176226/how-do-i-pass-extra-arguments-to-a-python-decorator

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.