web-dev-qa-db-ja.com

Django-例外処理のベストプラクティスとカスタマイズされたエラーメッセージの送信

私はDjangoアプリで適切な例外処理について考え始めています。私の目標は、可能な限りユーザーフレンドリーにすることです。ユーザーフレンドリーということで、ユーザーは常に正確に何が悪かったのかを詳細に説明します。 この投稿 に続いて、ベストプラクティスは

通常の応答にはステータス200のJSON応答を使用し、エラーに対して(適切な!)4xx/5xx応答を返します。これらはJSONペイロードも保持できるため、サーバー側でエラーに関する詳細を追加できます。

私の頭の中の答えよりもさらに多くの質問をすることで、この答えのキーワードでグーグルを試みました。

  1. どのエラーコード(400または500)を返すかをどのように決定しますか?つまり、Djangoには多くの定義済みエラータイプがあり、どのようにDjango例外タイプと400-500エラーコード間のマッピングを実装して例外処理を行うことができますかDRYとしてブロックし、可能な限り再利用可能?
  2. 投稿 で@Reorxによって提案されたミドルウェアのアプローチは実行可能と見なされますか? (答えはたった1つの賛成票を得たので、詳細を掘り下げてプロジェクトに実装することに消極的になりました。
  3. 最も重要なことは、誤った構文やnull値などの標準ではなく、ビジネスロジックに関連するエラーを発生させたい場合があることです。たとえば、法人にCEOがいない場合、ユーザーによる契約の追加を禁止できます。この場合のエラーステータスはどうあるべきですか?また、ユーザーに対するエラーの詳細な説明とともにエラーをスローするにはどうすればよいですか?

単純な視点で考えてみましょう

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

まず最初に、どのエラーを公開したいかを考える必要があります。

  • 通常、4xxエラー(クライアント側に起因するエラー)が開示されるため、ユーザーはリクエストを修正できます。

  • 一方、5xxエラー(サーバー側に起因するエラー)は通常、情報なしでのみ表示されます。それらに対する私の意見では、 Sentry などのツールを使用する必要があります。このエラーを監視して解決してください。

正しいAjaxリクエストの場合、これを念頭に置いて、メッセージと説明(該当する場合)のように何が起こったのかを理解するために、ステータスコードとjsonを返す必要があります。

目的がajaxを使用して情報を送信することである場合、必要なものに form を設定することをお勧めします。これにより、検証プロセスの一部を簡単に実行できます。例ではこれが当てはまると思います。

最初-リクエストは正しいですか?

_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-フォームにエラーがありますか?

_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
_

フィールドごとにエラーを取得することもできるので、フォーム自体でより良い方法で表示できます。

Third-リクエストを処理しましょう

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

定義する例外に応じて、異なるコードが必要になる場合があります。 Wikipedia に移動して、リストを確認します。応答もコードが異なることを忘れないでください。データベースに何かを追加する場合は、_201_を返す必要があります。情報を取得したばかりの場合は、GETリクエストを探していました。

質問への回答

  1. Django例外は、処理されない場合は500エラーを返します。例外が発生することがわからない場合は、サーバーのエラーであるためです。 404およびログイン要件を除き、すべてに対して_try catch_ブロックを実行します。 (404の場合、それを上げることができます。また、_@login_required_または必要な許可があればDjangoは何もせずに適切なコードで応答します)。

  2. 私はこのアプローチに完全には同意しません。あなたが言ったように、エラーは明示的でなければならないので、何が起こると何を説明するかを常に知って、実行された操作に依存できるようにする必要があります。

  3. そのためには、400エラーでかまいません。エラーコードはあなたとjsコードのためであるため、一貫性を保つために、理由を説明するだけでよいのです。

  4. (例が提供されています)-_text_view_には、3番目の例のように_test_method_が必要です。

テストメソッドの構造は次のとおりです。

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

私の例では:

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

その要求の前に何かが必要な場合、クライアントはそれを認識し、最初にそれを行うようにユーザーに要求する必要があるため、ビジネスロジック違反をクライアントエラーと見なしました。 ( エラー定義 から):

400(Bad Request)ステータスコードは、クライアントエラー(たとえば、不正な形式のリクエスト構文、無効なリクエスト)として認識される何かが原因で、サーバーがリクエストを処理できない、または処理しないことを示します
メッセージフレーミング、または不正なリクエストルーティング)。

ところで、 ユーザー定義の例外に関するPythonドキュメント を見ることができるので、適切なエラーメッセージを出すことができます。この例の背後にある考え方は、BusinessLogicViolationexceptionを、生成された場所に応じてmy_business_logic_is_violated()の異なるメッセージで発生させることです。

16
NBajanca

ステータスコードは、HTTP標準で非常によく定義されています。 Wikipediaで非常に読みやすいリスト を見つけることができます。基本的に、4XX範囲のエラーは、クライアントによって発生したエラーです。つまり、存在しないリソースを要求した場合などです。サーバー側でエラーが発生した場合、5XX範囲のエラーが返されます。

ポイント番号3に関しては、前提条件が満たされていない場合、たとえば428 Precondition Requiredが、サーバーが構文エラーを発生させると5XXエラーを返します。

例の問題の1つは、サーバーが特定の例外を発生させない限り、応答が返されないことです。つまり、コードが正常に実行され、例外が発生しない場合、メッセージもステータスコードもクライアントに明示的に送信されません。これは、コードのその部分をできるだけ汎用的にするために、finallyブロックを介して処理できます。

あなたの例に従って:

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)

ただし、コメントで述べたように、両方の検証を同じ方法で、つまり例外を介して行う方がより理にかなっています:

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