Manejo de los errores de la aplicación

Las aplicaciones fallan, los servidores fallan. Tarde o temprano verás una excepción en producción. Incluso si tu código es 100% correcto, verás excepciones de vez en cuando. ¿Por qué? Porque todo lo demás fallará. Aquí hay algunas situaciones en las que un código perfectamente correcto puede llevar a errores en el servidor:

  • el cliente terminó la solicitud antes de tiempo y la aplicación todavía estaba leyendo de los datos entrantes

  • el servidor de la base de datos estaba sobrecargado y no podía manejar la consulta

  • un sistema de archivos está lleno

  • un disco duro se ha estropeado

  • un servidor backend sobrecargado

  • un error de programación en una biblioteca que está utilizando

  • falló la conexión de red del servidor con otro sistema

Y eso es sólo una pequeña muestra de los problemas a los que puede enfrentarse. Así que, ¿cómo podemos hacer frente a este tipo de problemas? Por defecto, si tu aplicación se ejecuta en modo de producción y se produce una excepción, Flask te mostrará una página muy simple y registrará la excepción en el logger.

Pero hay más que puede hacer, y cubriremos algunas configuraciones mejores para lidiar con los errores, incluyendo excepciones personalizadas y herramientas de terceros.

Herramientas de registro de errores

El envío de correos de error, aunque sólo sea para los críticos, puede llegar a ser abrumador si hay un número suficiente de usuarios que se encuentran con el error y los archivos de registro normalmente nunca se miran. Por eso recomendamos utilizar Sentry para tratar los errores de la aplicación. Está disponible como proyecto con código fuente en GitHub y también está disponible como versión alojada que puedes probar gratuitamente. Sentry agrega los errores duplicados, captura el seguimiento completo de la pila y las variables locales para la depuración, y le envía correos en función de nuevos errores o umbrales de frecuencia.

Para utilizar Sentry es necesario instalar el cliente sentry-sdk con dependencias extra de flask.

$ pip install sentry-sdk[flask]

Y luego añade esto a tu aplicación Flask:

import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration

sentry_sdk.init('YOUR_DSN_HERE', integrations=[FlaskIntegration()])

El valor YOUR_DSN_HERE debe ser sustituido por el valor DSN que obtiene de su instalación de Sentry.

Después de la instalación, los fallos que conducen a un Error Interno del Servidor son reportados automáticamente a Sentry y desde allí puede recibir notificaciones de error.

Vea también:

Manejadores de errores

Cuando se produce un error en Flask, se devuelve un código de estado HTTP apropiado. 400-499 indican errores con los datos de la solicitud del cliente, o sobre los datos solicitados. 500-599 indican errores con el servidor o la aplicación en sí.

Es posible que desee mostrar páginas de error personalizadas al usuario cuando se produce un error. Esto se puede hacer registrando manejadores de error.

Un gestor de errores es una función que devuelve una respuesta cuando se produce un tipo de error, de forma similar a como una vista es una función que devuelve una respuesta cuando una URL de solicitud coincide. Se le pasa la instancia del error que se está manejando, que probablemente sea una HTTPException.

El código de estado de la respuesta no se ajustará al código del manejador. Asegúrese de proporcionar el código de estado HTTP apropiado cuando devuelva una respuesta de un manejador.

Registrando

Registra los manejadores decorando una función con errorhandler(). O usa register_error_handler() para registrar la función más tarde. Recuerda establecer el código de error al devolver la respuesta.

@app.errorhandler(werkzeug.exceptions.BadRequest)
def handle_bad_request(e):
    return 'bad request!', 400

# or, without the decorator
app.register_error_handler(400, handle_bad_request)

Las subclases de werkzeug.exceptions.HTTPException como BadRequest y sus códigos HTTP son intercambiables al registrar los manejadores. (BadRequest.code == 400)

Los códigos HTTP no estándar no pueden ser registrados por el código porque no son conocidos por Werkzeug. En su lugar, defina una subclase de HTTPException con el código apropiado y registre y lance esa clase de excepción.

class InsufficientStorage(werkzeug.exceptions.HTTPException):
    code = 507
    description = 'Not enough storage space.'

app.register_error_handler(InsufficientStorage, handle_507)

raise InsufficientStorage()

Los manejadores pueden registrarse para cualquier clase de excepción, no sólo para las subclases HTTPException o los códigos de estado HTTP. Los manejadores pueden ser registrados para una clase específica, o para todas las subclases de una clase padre.

Manejando

Cuando construyas una aplicación Flask te encontrarás con excepciones. Si alguna parte de tu código se rompe mientras manejas una petición (y no tienes ningún controlador de errores registrado), un «500 Internal Server Error» (InternalServerError) será devuelto por defecto. Del mismo modo, se producirá el error «404 Not Found» (NotFound) si se envía una solicitud a una ruta no registrada. Si una ruta recibe un método de solicitud no permitido, se producirá un error «405 Method Not Allowed» (MethodNotAllowed). Todas estas son subclases de HTTPException y se proporcionan por defecto en Flask.

Flask te da la posibilidad de lanzar cualquier excepción HTTP registrada por Werkzeug. Sin embargo, las excepciones HTTP por defecto devuelven simples páginas de excepción. Es posible que quieras mostrar páginas de error personalizadas al usuario cuando se produzca un error. Esto se puede hacer registrando manejadores de error.

Cuando Flask atrapa una excepción mientras maneja una solicitud, primero se busca por código. Si no se registra ningún manejador para el código, Flask busca el error por su jerarquía de clases; se elige el manejador más específico. Si no se registra ningún manejador, las subclases HTTPException muestran un mensaje genérico sobre su código, mientras que otras excepciones se convierten en un genérico «500 Internal Server Error».

Por ejemplo, si se lanza una instancia de ConnectionRefusedError, y se registra un manejador para ConnectionError y ConnectionRefusedError, se llama al manejador más específico de ConnectionRefusedError con la instancia de excepción para generar la respuesta.

Los manejadores registrados en el blueprint tienen prioridad sobre los registrados globalmente en la aplicación, asumiendo que un blueprint está manejando la solicitud que genera la excepción. Sin embargo, el blueprint no puede manejar errores de enrutamiento 404 porque el 404 se produce en el nivel de enrutamiento antes de que el blueprint pueda ser determinado.

Manejadores genéricos de excepciones

Es posible registrar manejadores de errores para clases base muy genéricas como HTTPException o incluso Exception. Sin embargo, hay que tener en cuenta que éstos atraparán más de lo que se espera.

Por ejemplo, un manejador de errores para HTTPException podría ser útil para convertir las páginas de errores HTML por defecto en JSON. Sin embargo, este manejador se activará para cosas que no causes directamente, como los errores 404 y 405 durante el enrutamiento. Asegúrate de elaborar tu manejador con cuidado para no perder información sobre el error HTTP.

from flask import json
from werkzeug.exceptions import HTTPException

@app.errorhandler(HTTPException)
def handle_exception(e):
    """Return JSON instead of HTML for HTTP errors."""
    # start with the correct headers and status code from the error
    response = e.get_response()
    # replace the body with JSON
    response.data = json.dumps({
        "code": e.code,
        "name": e.name,
        "description": e.description,
    })
    response.content_type = "application/json"
    return response

Un manejador de errores para Exception podría parecer útil para cambiar cómo se presentan al usuario todos los errores, incluso los no manejados. Sin embargo, esto es similar a hacer except Exception: en Python, capturará todos los errores no manejados, incluyendo todos los códigos de estado HTTP.

En la mayoría de los casos será más seguro registrar manejadores para excepciones más específicas. Dado que las instancias de HTTPException son respuestas válidas de WSGI, también podrías pasarlas directamente.

from werkzeug.exceptions import HTTPException

@app.errorhandler(Exception)
def handle_exception(e):
    # pass through HTTP errors
    if isinstance(e, HTTPException):
        return e

    # now you're handling non-HTTP exceptions only
    return render_template("500_generic.html", e=e), 500

Los manejadores de errores siguen respetando la jerarquía de clases de excepción. Si registras manejadores tanto para HTTPException como para Exception, el manejador de Exception no manejará subclases de HTTPException porque el manejador de HTTPException es más específico.

Excepciones no controladas

Cuando no hay un manejador de errores registrado para una excepción, se devolverá un Error Interno del Servidor 500 en su lugar. Véase flask.Flask.handle_exception() para obtener información sobre este comportamiento.

Si hay un gestor de errores registrado para InternalServerError, éste será invocado. A partir de Flask 1.1.0, a este manejador de errores siempre se le pasará una instancia de InternalServerError, no el error original no manejado.

El error original está disponible como e.original_exception.

Un manejador de errores para «500 Internal Server Error» será pasado por excepciones no capturadas además de los errores 500 explícitos. En el modo de depuración, no se utilizará un manejador para «500 Internal Server Error». En su lugar, se mostrará el depurador interactivo.

Páginas de error personalizadas

A veces, cuando se construye una aplicación Flask, es posible que se quiera lanzar una HTTPException para indicar al usuario que algo va mal en la petición. Afortunadamente, Flask viene con una práctica función abort() que aborta una petición con un error HTTP de werkzeug como se desee. También te proporcionará una página de error en blanco y negro con una descripción básica, pero nada del otro mundo.

Dependiendo del código de error es menos o más probable que el usuario vea realmente dicho error.

Considere el código de abajo, podríamos tener una ruta de perfil de usuario, y si el usuario no pasa un nombre de usuario podemos levantar un «400 Bad Request». Si el usuario pasa un nombre de usuario y no podemos encontrarlo, lanzamos un «404 Not Found».

from flask import abort, render_template, request

# a username needs to be supplied in the query args
# a successful request would be like /profile?username=jack
@app.route("/profile")
def user_profile():
    username = request.arg.get("username")
    # if a username isn't supplied in the request, return a 400 bad request
    if username is None:
        abort(400)

    user = get_user(username=username)
    # if a user can't be found by their username, return 404 not found
    if user is None:
        abort(404)

    return render_template("profile.html", user=user)

Aquí hay otro ejemplo de implementación para una excepción «404 Page Not Found»:

from flask import render_template

@app.errorhandler(404)
def page_not_found(e):
    # note that we set the 404 status explicitly
    return render_template('404.html'), 404

Cuando se utiliza Fábricas de aplicaciones:

from flask import Flask, render_template

def page_not_found(e):
  return render_template('404.html'), 404

def create_app(config_filename):
    app = Flask(__name__)
    app.register_error_handler(404, page_not_found)
    return app

Un ejemplo de plantilla podría ser este:

{% extends "layout.html" %}
{% block title %}Page Not Found{% endblock %}
{% block body %}
  <h1>Page Not Found</h1>
  <p>What you were looking for is just not there.
  <p><a href="{{ url_for('index') }}">go somewhere nice</a>
{% endblock %}

Otros ejemplos

Los ejemplos anteriores no serían en realidad una mejora de las páginas de excepción por defecto. Podemos crear una plantilla 500.html personalizada como esta:

{% extends "layout.html" %}
{% block title %}Internal Server Error{% endblock %}
{% block body %}
  <h1>Internal Server Error</h1>
  <p>Oops... we seem to have made a mistake, sorry!</p>
  <p><a href="{{ url_for('index') }}">Go somewhere nice instead</a>
{% endblock %}

Se puede implementar al renderizar la plantilla en «500 Internal Server Error»:

from flask import render_template

@app.errorhandler(500)
def internal_server_error(e):
    # note that we set the 500 status explicitly
    return render_template('500.html'), 500

Cuando se utiliza Fábricas de aplicaciones:

from flask import Flask, render_template

def internal_server_error(e):
  return render_template('500.html'), 500

def create_app():
    app = Flask(__name__)
    app.register_error_handler(500, internal_server_error)
    return app

Cuando se utiliza Aplicaciones modulares con Blueprints:

from flask import Blueprint

blog = Blueprint('blog', __name__)

# as a decorator
@blog.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

# or with register_error_handler
blog.register_error_handler(500, internal_server_error)

Manejadores de errores de Blueprint

En Aplicaciones modulares con Blueprints, la mayoría de los manejadores de errores funcionarán como se espera. Sin embargo, hay una advertencia sobre los manejadores de las excepciones 404 y 405. Estos manejadores de error sólo se invocan desde una sentencia raise apropiada o una llamada a abort en otra de las funciones de vista del blueprint; no se invocan, por ejemplo, por un acceso a una URL no válida.

Esto se debe a que el blueprint no es «dueño» de un determinado espacio URL, por lo que la instancia de la aplicación no tiene forma de saber qué manejador de errores del blueprint debe ejecutar si se le da una URL inválida. Si quieres ejecutar diferentes estrategias de manejo para estos errores basados en prefijos de URL, pueden ser definidos a nivel de aplicación usando el objeto proxy request.

from flask import jsonify, render_template

# at the application level
# not the blueprint level
@app.errorhandler(404)
def page_not_found(e):
    # if a request is in our blog URL space
    if request.path.startswith('/blog/'):
        # we return a custom blog 404 page
        return render_template("blog/404.html"), 404
    else:
        # otherwise we return our generic site-wide 404 page
        return render_template("404.html"), 404

@app.errorhandler(405)
def method_not_allowed(e):
    # if a request has the wrong method to our API
    if request.path.startswith('/api/'):
        # we return a json saying so
        return jsonify(message="Method Not Allowed"), 405
    else:
        # otherwise we return a generic site-wide 405 page
        return render_template("405.html"), 405

Devolución de errores de la API como JSON

Al construir APIs en Flask, algunos desarrolladores se dan cuenta de que las excepciones incorporadas no son lo suficientemente expresivas para las APIs y que el tipo de contenido text/html que emiten no es muy útil para los consumidores de la API.

Usando las mismas técnicas anteriores y jsonify() podemos devolver respuestas JSON a los errores de la API. abort() se llama con un parámetro description. El controlador de errores utilizará eso como el mensaje de error JSON, y establecerá el código de estado como 404.

from flask import abort, jsonify

@app.errorhandler(404)
def resource_not_found(e):
    return jsonify(error=str(e)), 404

@app.route("/cheese")
def get_one_cheese():
    resource = get_resource()

    if resource is None:
        abort(404, description="Resource not found")

    return jsonify(resource)

También podemos crear clases de excepción personalizadas. Por ejemplo, podemos introducir una nueva excepción personalizada para una API que puede tomar un mensaje adecuado legible por humanos, un código de estado para el error y alguna carga útil opcional para dar más contexto para el error.

Este es un ejemplo sencillo:

from flask import jsonify, request

class InvalidAPIUsage(Exception):
    status_code = 400

    def __init__(self, message, status_code=None, payload=None):
        super().__init__()
        self.message = message
        if status_code is not None:
            self.status_code = status_code
        self.payload = payload

    def to_dict(self):
        rv = dict(self.payload or ())
        rv['message'] = self.message
        return rv

@app.errorhandler(InvalidAPIUsage)
def invalid_api_usage(e):
    return jsonify(e.to_dict()), e.status_code

# an API app route for getting user information
# a correct request might be /api/user?user_id=420
@app.route("/api/user")
def user_api(user_id):
    user_id = request.arg.get("user_id")
    if not user_id:
        raise InvalidAPIUsage("No user id provided!")

    user = get_user(user_id=user_id)
    if not user:
        raise InvalidAPIUsage("No such user!", status_code=404)

    return jsonify(user.to_dict())

Una vista puede ahora lanzar esa excepción con un mensaje de error. Además, se puede proporcionar una carga útil adicional como un diccionario a través del parámetro payload.

Registro

Consulte Registro para obtener información sobre cómo registrar las excepciones, por ejemplo, enviándolas por correo electrónico a los administradores.

Depurando

Consulte Depuración de errores de la aplicación para obtener información sobre cómo depurar errores en desarrollo y producción.