web-dev-qa-db-fra.com

Django - meilleure gestion des exceptions et envoi d'un message d'erreur personnalisé

Je commence à penser à la gestion appropriée des exceptions dans mon Django application, et mon objectif est de la rendre aussi conviviale que possible. Par la convivialité, j'implique que l'utilisateur doit toujours obtenir une clarification détaillée sur ce qui s'est exactement passé. Suite à ce post , la meilleure pratique est de

utilisez une réponse JSON avec le statut 200 pour vos réponses normales et renvoyez une réponse (appropriée!) 4xx/5xx pour les erreurs. Ceux-ci peuvent également transporter la charge utile JSON, de sorte que votre côté serveur peut ajouter des détails supplémentaires sur l'erreur.

J'ai essayé de google par les mots clés de cette réponse, en ayant encore plus de questions que de réponses dans ma tête.

  1. Comment puis-je décider quel code d'erreur - 400 ou 500 - retourner? Je veux dire, Django a de nombreux types d'erreur prédéfinis, et comment puis-je implémenter ce mappage entre Django types d'exception et code d'erreur 400-500 pour effectuer la gestion des exceptions bloque comme DRY et réutilisable autant que possible?
  2. L'approche avec le middleware suggérée par @Reorx dans le post peut-elle être considérée comme viable? (La réponse n'a reçu qu'un seul vote positif, ce qui me rend réticent à me plonger dans les détails et à l'implémenter dans mon projet
  3. Plus important encore, je souhaite parfois soulever une erreur liée à la logique métier, plutôt qu'une syntaxe incorrecte ou quelque chose de standard comme une valeur nulle. Par exemple, s'il n'y a pas de PDG dans mon entité juridique, je voudrais peut-être interdire à l'utilisateur d'ajouter un contrat. Quel devrait être le statut d'erreur dans ce cas, et comment envoyer une erreur avec mon explication détaillée de l'erreur pour l'utilisateur?

Considérons-le sur une vue simple

def test_view (request):

   try:
          # Some code .... 
          if my_business_logic_is_violated():
              # How do I raise the error
              error_msg = "You violated bussiness logic because..."
              # How do I pass error_msg 
          my_response = {'my_field' : value}
  except ExpectedError as e:
          # what is the most appropriate way to pass both error status and custom message
          # How do I list all possible error types here (instead of ExpectedError to make the exception handling block as DRY and reusable as possible
      return JsonResponse({'status':'false','message':message}, status=500)
18
Edgar Navasardyan

Tout d'abord, vous devez réfléchir aux erreurs que vous souhaitez exposer:

  • Habituellement, les erreurs 4xx (erreurs attribuées au côté client) sont divulguées afin que l'utilisateur puisse corriger la demande.

  • D'un autre côté, les erreurs 5xx (erreurs attribuées côté serveur) ne sont généralement présentées que sans information. À mon avis, pour ceux que vous devriez utiliser des outils comme Sentry surveillez et résolvez ces erreurs, qui peuvent avoir des problèmes de sécurité intégrés.

Ayant cela à l'esprit à mon avis pour une demande Ajax correcte, vous devez renvoyer un code d'état, puis quelques json pour aider à comprendre ce qui s'est passé comme un message et une explication (le cas échéant).

Si votre objectif est d'utiliser ajax pour soumettre des informations, je vous suggère de définir un formulaire pour ce que vous voulez. De cette façon, vous passez facilement une partie du processus de validation. Je suppose que c'est le cas dans l'exemple.

Premier - La demande est-elle correcte?

def test_view(request):
    message = None
    explanation = None
    status_code = 500
    # First, is the request correct?
    if request.is_ajax() and request.method == "POST":
        ....
    else: 
        status_code = 400
        message = "The request is not valid."
        # You should log this error because this usually means your front end has a bug.
        # do you whant to explain anything?
        explanation = "The server could not accept your request because it was not valid. Please try again and if the error keeps happening get in contact with us."

    return JsonResponse({'message':message,'explanation':explanation}, status=status_code)

Deuxième - Y a-t-il des erreurs dans le formulaire?

form = TestForm(request.POST)
if form.is_valid():
    ...
else:
    message = "The form has errors"
    explanation = form.errors.as_data()
    # Also incorrect request but this time the only flag for you should be that maybe JavaScript validation can be used.
    status_code = 400

Vous pouvez même obtenir une erreur champ par champ afin de mieux vous présenter dans le formulaire lui-même.

Troisième - Traitons la demande

        try:
            test_method(form.cleaned_data)
        except `PermissionError` as e:
            status_code= 403
            message= "Your account doesn't have permissions to go so far!"
        except `Conflict` as e:
            status_code= 409
            message= "Other user is working in the same information, he got there first"
        ....
        else:
            status_code= 201
            message= "Object created with success!"

Selon les exceptions que vous définissez, différents codes peuvent être requis. Allez sur Wikipedia et vérifiez la liste. N'oubliez pas que la réponse varie également dans le code. Si vous ajoutez quelque chose à la base de données, vous devez renvoyer un 201. Si vous venez de recevoir des informations, vous cherchiez une demande GET.

Répondre aux questions

  1. Les exceptions Django renverront 500 erreurs si elles ne sont pas traitées, car si vous ne savez pas qu'une exception va se produire, c'est une erreur sur le serveur. À l'exception de 404 et des exigences de connexion, je ferais des blocs try catch Pour tout. (Pour 404, vous pouvez l'augmenter et si vous faites @login_required Ou une autorisation requise Django répondra avec le code approprié sans que vous ne fassiez rien).

  2. Je ne suis pas entièrement d'accord avec l'approche. Comme vous l'avez dit, les erreurs doivent être explicites, vous devez donc toujours savoir ce qui est censé se produire et comment l'expliquer, et le rendre fiable en fonction de l'opération effectuée.

  3. Je dirais qu'une erreur 400 est ok pour ça. C'est une mauvaise demande, il vous suffit d'expliquer pourquoi, le code d'erreur est pour vous et pour votre code js, soyez donc cohérent.

  4. (exemple fourni) - Dans le text_view, vous devriez avoir le test_method comme dans le troisième exemple.

La méthode d'essai doit avoir la structure suivante:

def test_method(validated_data):
    try: 
        my_business_logic_is_violated():
    catch BusinessLogicViolation:
        raise
    else:
        ... #your code

Le dans mon exemple:

   try:
        test_method(form.cleaned_data)
    except `BusinessLogicViolation` as e:
        status_code= 400
        message= "You violated the business logic"
        explanation = e.explanation
   ...

J'ai considéré la violation de la logique métier comme une erreur client car si quelque chose est nécessaire avant cette demande, le client doit en être conscient et demander à l'utilisateur de le faire en premier. (De la Définition d'erreur ):

Le code d'état 400 (mauvaise demande) indique que le serveur ne peut pas ou ne traitera pas la demande en raison de quelque chose qui est perçu comme une erreur client (par exemple, syntaxe de demande mal formée, demande non valide
cadrage des messages ou routage des demandes trompeuses).

Soit dit en passant, vous pouvez voir le Python Docs on User-defined Exceptions afin que vous puissiez donner des messages d'erreur appropriés. L'idée derrière cet exemple est que vous soulevez une BusinessLogicViolationexception avec un message différent dans my_business_logic_is_violated() selon l'endroit où il a été généré.

16
NBajanca

Les codes d'état sont très bien définis dans la norme HTTP. Vous pouvez trouver une liste très lisible sur Wikipedia . Fondamentalement, les erreurs dans la plage 4XX sont des erreurs commises par le client, c'est-à-dire s'il demande une ressource qui n'existe pas, etc. Les erreurs dans la plage 5XX doivent être renvoyées si une erreur est rencontrée côté serveur.

En ce qui concerne le point numéro 3, vous devez sélectionner une erreur 4XX pour le cas où une condition préalable n'a pas été remplie, par exemple 428 Precondition Required, mais renvoie une erreur 5XX lorsqu'un serveur déclenche une erreur de syntaxe.

L'un des problèmes de votre exemple est qu'aucune réponse n'est renvoyée à moins que le serveur ne déclenche une exception spécifique, c'est-à-dire lorsque le code s'exécute normalement et qu'aucune exception n'est déclenchée, ni le message ni le code d'état ne sont explicitement envoyés au client. Cela peut être pris en charge via un bloc finally, pour rendre cette partie du code aussi générique que possible.

Selon votre exemple:

def test_view (request):
   try:
       # Some code .... 
       status = 200
       msg = 'Everything is ok.'
       if my_business_logic_is_violated():
           # Here we're handling client side errors, and hence we return
           # status codes in the 4XX range
           status = 428
           msg = 'You violated bussiness logic because a precondition was not met'.
   except SomeException as e:
       # Here, we assume that exceptions raised are because of server
       # errors and hence we return status codes in the 5XX range
       status = 500
       msg = 'Server error, yo'
   finally:
       # Here we return the response to the client, regardless of whether
       # it was created in the try or the except block
       return JsonResponse({'message': msg}, status=status)

Cependant, comme indiqué dans les commentaires, il serait plus logique de faire les deux validations de la même manière, c'est-à-dire via des exceptions, comme ceci:

def test_view (request):
   try:
       # Some code .... 
       status = 200
       msg = 'Everything is ok.'
       if my_business_logic_is_violated():
           raise MyPreconditionException()
   except MyPreconditionException as e:
       # Here we're handling client side errors, and hence we return
       # status codes in the 4XX range
       status = 428
       msg = 'Precondition not met.'
   except MyServerException as e:
       # Here, we assume that exceptions raised are because of server
       # errors and hence we return status codes in the 5XX range
       status = 500
       msg = 'Server error, yo.'
   finally:
       # Here we return the response to the client, regardless of whether
       # it was created in the try or the except block
       return JsonResponse({'message': msg}, status=status)
5
kreld