Python Django - 异常处理最佳实践和发送自定义错误消息

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/38847441/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-19 21:30:04  来源:igfitidea点击:

Django - exception handling best practice and sending customized error message

pythonjsonajaxdjango

提问by Edgar Navasardyan

I am starting to think about appropriate exception handling in my Django app, and my goal is to make it as user-friendly, as possible. By user-friendliness, I imply that the user must always get a detailed clarification as to what exactly went wrong. Following on this post, the best practice is to

我开始考虑在我的 Django 应用程序中进行适当的异常处理,我的目标是尽可能使其用户友好。通过用户友好性,我的意思是用户必须始终详细了解到底出了什么问题。在这篇文章之后,最佳实践是

use a JSON response with status 200 for your normal responses and return an (appropriate!) 4xx/5xx response for errors. These can carry JSON payload, too, so your server side can add additional details about the error.

使用状态为 200 的 JSON 响应作为您的正常响应,并为错误返回(适当的!)4xx/5xx 响应。这些也可以携带 JSON 有效负载,因此您的服务器端可以添加有关错误的其他详细信息。

I tried to google by the key words in this answer, by still have more questions than answers in my head.

我试图通过这个答案中的关键词进行谷歌搜索,因为我脑子里的问题仍然比答案多。

  1. How do I decide upon which error code - 400 or 500 - to return? I mean, Django has many predefined error types, and how can I implement this mapping between Django exception types and 400-500 error code to make the exception handling blocks as DRY and reusable as possible?
  2. Can the approach with middleware suggested by @Reorx in the postbe considered viable ? ( The answer got only one upvote, thus making me reluctant to delve into details and implement it in my project
  3. Most importantly, sometimes I may wish to raise an error related to business logic, rather than incorrect syntax or something standard like null value. For example, if there's no CEO in my legal entity, I might want to prohibit the user from adding a contract. What should be the error status in this case, and how do I throw an error with my detailed explanation of the error for the user?
  1. 我如何决定返回哪个错误代码 - 400 或 500?我的意思是,Django 有许多预定义的错误类型,我如何实现 Django 异常类型和 400-500 错误代码之间的这种映射,以使异常处理块尽可能 DRY 和可重用?
  2. @Reorx 在帖子中建议的中间件方法是否可行?(答案只有一个赞成票,因此我不愿意深入研究细节并在我的项目中实施它
  3. 最重要的是,有时我可能希望引发与业务逻辑相关的错误,而不是错误的语法或诸如空值之类的标准。例如,如果我的法律实体中没有 CEO,我可能想禁止用户添加合同。在这种情况下,错误状态应该是什么,我如何向用户抛出错误并详细解释错误?

Let us consider it on a simple view

让我们从一个简单的角度考虑它

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)

采纳答案by NBajanca

First of all you should think on what errors you want to expose:

首先,您应该考虑要公开哪些错误:

  • Usually 4xx errors (Errors that are attributed to the client-side) are disclosed so the user may correct the request.

  • On the other side, 5xx errors (Errors that are attributed to the server-side) are usually only presented without information. In my opinion for those you should use tools like Sentrydo monitor and resolve this errors, that may have security issues embedded in them.

  • 通常会公开 4xx 错误(归因于客户端的错误),以便用户可以更正请求。

  • 另一方面,5xx 错误(归因于服务器端的错误)通常仅在没有信息的情况下呈现。在我看来,对于那些应该使用Sentry 之类的工具来监控和解决这些错误的人来说,这些错误可能嵌入了安全问题。

Having this is mind in my opinion for a correct Ajax request you should return a status code and then some json to help understand what happened like a message and an explanation (when applicable).

在我看来,对于正确的 Ajax 请求,您应该返回一个状态代码,然后返回一些 json 以帮助理解发生了什么,例如消息和解释(如果适用)。

If your objective is to use ajax to submit information I suggest setting a formfor what you want. This way you get past some of the validation process with ease. I will assume the case is this in the example.

如果您的目标是使用 ajax 提交信息,我建议您根据需要设置一个表单。通过这种方式,您可以轻松通过一些验证过程。我将假设在示例中就是这种情况。

First- Is the request correct?

第一- 请求是否正确?

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)

Second- Are there errors in the form?

第二- 表格中是否有错误?

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

You may even get error field by field so you may presented in a better way in the form itself.

您甚至可能会逐个字段地获取错误,因此您可以在表单本身中以更好的方式呈现。

Third- Let's process the request

第三- 让我们处理请求

        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!"

Depending on the exceptions you define, different codes may be required. Go to Wikipediaand check the list. Don't forget that response also vary in code. If you add something to the database you should return a 201. If you just got information then you were looking for a GET request.

根据您定义的例外情况,可能需要不同的代码。转到维基百科并查看列表。不要忘记响应也因代码而异。如果你向数据库添加了一些东西,你应该返回一个201. 如果您刚刚获得信息,那么您正在寻找 GET 请求。

Responding to the questions

回答问题

  1. Django exceptions will return 500 errors if not dealt with, because if you don't know that an exception is going to happen then it is an error in the server. With exception to 404 and login requirements I would do try catchblocks for everything. (For 404 you may raise it and if you do @login_requiredor a permission required django will respond with the appropriate code without you doing anything).

  2. I don't agree completely to the approach. As you said errors should be explicit so you should know allways what is suppose to happen and how to explain it, and make it dependable on the operation performed.

  3. I would say a 400 error is ok for that. It is a bad request you just need to explain why, the error code is for you and for your js code so just be consistent.

  4. (example provided) - In the text_viewyou should have the test_methodas in the third example.

  1. 如果不处理,Django 异常将返回 500 错误,因为如果您不知道将要发生异常,那么它就是服务器中的错误。除了 404 和登录要求之外,我会try catch为所有内容设置块。(对于 404,您可以提出它,如果您这样做@login_required或需要许可,django 将响应适当的代码,而无需您做任何事情)。

  2. 我不完全同意这种方法。正如您所说的错误应该是明确的,因此您应该始终知道假设会发生什么以及如何解释它,并使其依赖于所执行的操作。

  3. 我会说 400 错误是可以的。这是一个糟糕的请求,你只需要解释为什么,错误代码是给你和你的 js 代码的,所以要保持一致。

  4. (提供了示例) - 在text_view您应该拥有test_method第三个示例中的 。

Test method should have the following structure:

测试方法应具有以下结构:

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

The in my example:

在我的例子中:

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

I considered the business logic violation to be a Client Error because if something is needed before that request the client should be aware of that and ask the user to do it first. (From the Error Definition):

我认为业务逻辑违规是客户端错误,因为如果在该请求之前需要某些东西,客户端应该意识到这一点并要求用户先做。(来自错误定义):

The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request
message framing, or deceptive request routing).

400 (Bad Request) 状态码表示服务器由于被认为是客户端错误(例如,格式错误的请求语法、无效的请求
消息帧或欺骗性的请求路由)而无法或不会处理请求。

By the way, you can see the Python Docs on User-defined Exceptionsso you may give appropriate error messages. The idea behind this example is that you raise a BusinessLogicViolationexception with a different message in my_business_logic_is_violated()according to the place where it was generated.

顺便说一句,您可以查看有关用户定义异常Python 文档,以便您可以提供适当的错误消息。这个例子背后的想法是你根据它的生成位置引发一个BusinessLogicViolation带有不同消息的异常my_business_logic_is_violated()

回答by kreld

The status codes are very well defined in the HTTP standard. You can find a very readable list on Wikipedia. Basically the errors in the 4XX range are errors made by the client, i.e. if they request a resource that doesn't exist, etc. The errors in the 5XX range should be returned if an error is encountered server side.

状态代码在 HTTP 标准中有很好的定义。您可以在 Wikipedia 上找到一个非常易读的列表。基本上,4XX 范围内的错误是客户端产生的错误,即如果他们请求不存在的资源等。如果服务器端遇到错误,则应返回 5XX 范围内的错误。

With regards to point number 3, you should pick a 4XX error for the case where a precondition has not been met, for example 428 Precondition Required, but return a 5XX error when a server raises a syntax error.

对于第 3 点,您应该在未满足前提条件的情况下选择 4XX 错误,例如428 Precondition Required,但在服务器引发语法错误时返回 5XX 错误。

One of the problems with your example is that no response is returned unless the server raises a specific exception, i.e. when the code executes normally and no exception is raised, neither the message nor the status code is explicitly sent to the client. This can be taken care of via a finally block, to make that part of the code as generic as possible.

您的示例的问题之一是除非服务器引发特定异常,否则不会返回任何响应,即当代码正常执行且未引发异常时,消息和状态代码都不会显式发送到客户端。这可以通过 finally 块来处理,以使该部分代码尽可能通用。

As per your example:

根据您的示例:

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)

However, as stated in the comments it would make more sense to do both validations the same way, i.e. via exceptions, like so:

但是,如评论中所述,以相同的方式进行两种验证会更有意义,即通过异常,如下所示:

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)