Traitement des erreurs d’application

Les applications échouent, les serveurs échouent. Tôt ou tard, vous verrez une exception en production. Même si votre code est 100% correct, vous verrez toujours des exceptions de temps en temps. Pourquoi ? Parce que tout ce qui est impliqué échoue. Voici quelques situations où un code parfaitement correct peut entraîner des erreurs de serveur :

  • le client a mis fin prématurément à la requête et l’application était toujours en train de lire les données entrantes

  • le serveur de base de données était surchargé et ne pouvait pas traiter la requête

  • un système de fichiers est plein

  • un disque dur est tombé en panne

  • un serveur dorsal surchargé

  • une erreur de programmation dans une bibliothèque que vous utilisez

  • la connexion réseau du serveur à un autre système a échoué

Et ce n’est qu’un petit échantillon des problèmes auxquels vous pourriez être confronté. Alors comment faire face à ce genre de problème ? Par défaut, si votre application fonctionne en mode production, et qu’une exception est levée, Flask affichera une page très simple pour vous et enregistrera l’exception dans le logger.

Mais vous pouvez faire plus, et nous couvrirons quelques meilleures configurations pour traiter les erreurs, y compris les exceptions personnalisées et les outils tiers.

Outils de journalisation des erreurs

L’envoi de mails d’erreur, même si c’est juste pour les erreurs critiques, peut devenir accablant si suffisamment d’utilisateurs sont affectés par l’erreur et les fichiers journaux ne sont généralement jamais regardés. C’est pourquoi nous recommandons d’utiliser Sentry pour traiter les erreurs d’application. Il est disponible en tant que projet disponible en source sur GitHub et est également disponible en tant que version hébergée que vous pouvez essayer gratuitement. Sentry agrège les erreurs dupliquées, capture la trace complète de la pile et les variables locales pour le débogage, et vous envoie des mails en fonction des nouvelles erreurs ou des seuils de fréquence.

Pour utiliser Sentry, vous devez installer le client sentry-sdk avec des dépendances flask supplémentaires.

$ pip install sentry-sdk[flask]

Et puis ajoutez ça à votre application Flask :

import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration

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

La valeur YOUR_DSN_HERE doit être remplacée par la valeur DSN que vous obtenez de votre installation Sentry.

Après l’installation, les échecs conduisant à une erreur interne du serveur sont automatiquement signalés à Sentry et de là, vous pouvez recevoir des notifications d’erreur.

Voir aussi :

Gestionnaires d’erreurs

Lorsqu’une erreur se produit dans Flask, un code d’état HTTP approprié sera renvoyé. 400-499 indiquent des erreurs avec les données de la requête du client, ou sur les données demandées. 500-599 indiquent des erreurs avec le serveur ou l’application elle-même.

Vous pouvez souhaiter afficher des pages d’erreur personnalisées à l’utilisateur lorsqu’une erreur se produit. Cela peut être fait en enregistrant des gestionnaires d’erreurs.

Un gestionnaire d’erreur est une fonction qui renvoie une réponse lorsqu’un type d’erreur est levé, de la même manière qu’une vue est une fonction qui renvoie une réponse lorsqu’une URL de requête est trouvée. On lui passe l’instance de l’erreur à gérer, qui est le plus souvent une HTTPException.

Le code d’état de la réponse ne sera pas défini comme le code du gestionnaire. Veillez à fournir le code d’état HTTP approprié lorsque vous renvoyez une réponse d’un gestionnaire.

Enregistrement

Enregistrez les gestionnaires en décorant une fonction avec errorhandler(). Ou utilisez register_error_handler() pour enregistrer la fonction plus tard. N’oubliez pas de définir le code d’erreur lors du retour de la réponse.

@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)

Les sous-classes werkzeug.exceptions.HTTPException comme BadRequest et leurs codes HTTP sont interchangeables lors de l’enregistrement des gestionnaires. (BadRequest.code == 400)

Les codes HTTP non standard ne peuvent pas être enregistrés par le code car ils ne sont pas connus par Werkzeug. Au lieu de cela, définissez une sous-classe de HTTPException avec le code approprié et enregistrez et levez cette classe d’exception.

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

app.register_error_handler(InsufficientStorage, handle_507)

raise InsufficientStorage()

Les gestionnaires peuvent être enregistrés pour n’importe quelle classe d’exception, pas seulement pour les sous-classes HTTPException ou les codes d’état HTTP. Les gestionnaires peuvent être enregistrés pour une classe spécifique, ou pour toutes les sous-classes d’une classe parente.

Gestion

Lorsque vous construisez une application Flask, vous rencontrerez des exceptions. Si une partie de votre code échoue pendant le traitement d’une requête (et que vous n’avez pas enregistré de gestionnaires d’erreurs), une erreur « 500 Internal Server Error » (InternalServerError) sera retournée par défaut. De même, une erreur « 404 Not Found » (NotFound) sera renvoyée si une requête est envoyée à une route non enregistrée. Si une route reçoit une méthode de requête non autorisée, une erreur « 405 Method Not Allowed » (MethodNotAllowed) sera générée. Toutes ces exceptions sont des sous-classes de HTTPException et sont fournies par défaut dans Flask.

Flask vous donne la possibilité de lever toute exception HTTP enregistrée par Werkzeug. Cependant, les exceptions HTTP par défaut renvoient des pages d’exception simples. Vous pourriez vouloir afficher des pages d’erreur personnalisées à l’utilisateur lorsqu’une erreur se produit. Cela peut être fait en enregistrant des gestionnaires d’erreurs.

Lorsque Flask attrape une exception lors du traitement d’une requête, elle est d’abord recherchée par code. Si aucun gestionnaire n’est enregistré pour le code, Flask recherche l’erreur par sa hiérarchie de classe ; le gestionnaire le plus spécifique est choisi. Si aucun gestionnaire n’est enregistré, les sous-classes HTTPException affichent un message générique sur leur code, tandis que les autres exceptions sont converties en un générique « 500 Internal Server Error ».

Par exemple, si une instance de ConnectionRefusedError est levée et qu’un gestionnaire est enregistré pour ConnectionError et ConnectionRefusedError, le gestionnaire plus spécifique ConnectionRefusedError est appelé avec l’instance d’exception pour générer la réponse.

Les gestionnaires enregistrés sur le blueprint sont prioritaires par rapport à ceux enregistrés globalement sur l’application, en supposant qu’un blueprint traite la requête qui soulève l’exception. Cependant, le Blueprint ne peut pas gérer les erreurs de routage 404 car le 404 se produit au niveau du routage avant que le blueprint puisse être déterminé.

Gestionnaires d’exceptions génériques

Il est possible d’enregistrer des gestionnaires d’erreurs pour des classes de base très génériques telles que HTTPException ou même Exception. Cependant, sachez que ces gestionnaires d’erreurs captureront plus d’erreurs que vous ne le pensez.

Par exemple, un gestionnaire d’erreur pour HTTPException pourrait être utile pour transformer les pages d’erreurs HTML par défaut en JSON. Cependant, ce gestionnaire se déclenchera pour des choses que vous ne provoquez pas directement, comme les erreurs 404 et 405 pendant le routage. Veillez à bien concevoir votre gestionnaire afin de ne pas perdre les informations relatives à l’erreur 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 gestionnaire d’erreur pour Exception peut sembler utile pour changer la façon dont toutes les erreurs, même celles qui ne sont pas gérées, sont présentées à l’utilisateur. Cependant, c’est similaire à l’utilisation de except Exception: en Python, cela capturera toutes les erreurs non gérées, y compris tous les codes d’état HTTP.

Dans la plupart des cas, il sera plus sûr d’enregistrer des gestionnaires pour des exceptions plus spécifiques. Comme les instances de HTTPException sont des réponses WSGI valides, vous pouvez aussi les passer directement.

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

Les gestionnaires d’erreurs respectent toujours la hiérarchie des classes d’exceptions. Si vous enregistrez des gestionnaires à la fois pour HTTPException et Exception, le gestionnaire Exception ne traitera pas les sous-classes HTTPException car le gestionnaire HTTPException est plus spécifique.

Exceptions non gérées

Si aucun gestionnaire d’erreur n’est enregistré pour une exception, une erreur de serveur interne 500 sera renvoyée à la place. Voir flask.Flask.handle_exception() pour des informations sur ce comportement.

S’il y a un gestionnaire d’erreur enregistré pour InternalServerError, il sera invoqué. Depuis Flask 1.1.0, ce gestionnaire d’erreur recevra toujours une instance de InternalServerError, et non l’erreur originale non gérée.

L’erreur originale est disponible en tant que e.original_exception.

Un gestionnaire d’erreur pour « 500 Internal Server Error » se verra transmettre des exceptions non capturées en plus des erreurs 500 explicites. En mode débogage, un gestionnaire d’erreur pour « 500 Internal Server Error » ne sera pas utilisé. Au lieu de cela, le débogueur interactif sera affiché.

Pages d’erreurs personnalisées

Parfois, lors de la construction d’une application Flask, vous pouvez vouloir lever une HTTPException pour signaler à l’utilisateur que quelque chose ne va pas avec la requête. Heureusement, Flask dispose d’une fonction abort() très pratique qui permet d’interrompre une requête avec une erreur HTTP de werkzeug comme souhaité. Elle vous fournira également une page d’erreur en noir et blanc avec une description de base, mais rien d’extraordinaire.

En fonction du code d’erreur, il est plus ou moins probable que l’utilisateur voit effectivement une telle erreur.

Considérez le code ci-dessous, nous pourrions avoir une route de profil d’utilisateur, et si l’utilisateur ne parvient pas à fournir un nom d’utilisateur, nous pouvons générer un « 400 Bad Request ». Si l’utilisateur fournit un nom d’utilisateur et que nous ne pouvons pas le trouver, nous envoyons un message « 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)

Voici un autre exemple de mise en œuvre d’une exception « 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

Lorsque vous utilisez Application Factories :

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 exemple de modèle pourrait être le suivant :

{% 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 %}

Autres exemples

Les exemples ci-dessus ne seraient pas vraiment une amélioration des pages d’exception par défaut. Nous pouvons créer un modèle 500.html personnalisé comme ceci :

{% 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 %}

Il peut être mis en œuvre en rendant le modèle sur « 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

Lorsque vous utilisez Application Factories :

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

Lorsque vous utilisez Modular Applications with 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)

Gestionnaires d’erreurs des blueprints

Dans Modular Applications with Blueprints, la plupart des gestionnaires d’erreurs fonctionnent comme prévu. Cependant, il y a une réserve concernant les gestionnaires des exceptions 404 et 405. Ces gestionnaires d’erreurs ne sont invoqués qu’à partir d’une déclaration appropriée raise ou d’un appel à abort dans une autre fonction de vue du blueprint ; ils ne sont pas invoqués par, par exemple, un accès URL invalide.

C’est parce que le blueprint ne « possède » pas un certain espace d’URL, donc l’instance de l’application n’a aucun moyen de savoir quel gestionnaire d’erreur de blueprint elle doit exécuter si elle reçoit une URL invalide. Si vous souhaitez exécuter des stratégies de traitement différentes pour ces erreurs en fonction des préfixes d’URL, elles peuvent être définies au niveau de l’application en utilisant l’objet 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

Retourner les erreurs d’API en JSON

Lors de la création d’API dans Flask, certains développeurs se rendent compte que les exceptions intégrées ne sont pas assez expressives pour les API et que le type de contenu text/html qu’elles émettent n’est pas très utile pour les utilisateurs d’API.

En utilisant les mêmes techniques que ci-dessus et jsonify(), nous pouvons renvoyer des réponses JSON aux erreurs d’API. abort() est appelé avec un paramètre description. Le gestionnaire d’erreur l’utilisera comme message d’erreur JSON, et définira le code d’état à 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)

Nous pouvons également créer des classes d’exceptions personnalisées. Par exemple, nous pouvons introduire une nouvelle exception personnalisée pour une API qui peut prendre un message lisible par l’homme, un code d’état pour l’erreur et des données utiles facultatives pour donner plus de contexte à l’erreur.

Il s’agit d’un exemple simple :

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())

# 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())

Une vue peut maintenant lever cette exception avec un message d’erreur. De plus, des données supplémentaires peuvent être fournies sous forme de dictionnaire via le paramètre payload.

Journalisation

Voir Journalisation pour des informations sur la façon d’enregistrer les exceptions, par exemple en les envoyant par courriel aux administrateurs.

Débogage

Voir Débogage des erreurs d’application pour des informations sur la façon de déboguer les erreurs en développement et en production.