Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django - exception handling best practice and sending customized error message

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

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.

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 post be 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?

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)
like image 620
Edgar Navasardyan Avatar asked Aug 09 '16 09:08

Edgar Navasardyan


People also ask

How do I create a custom exception in Django REST framework?

The generic views use the raise_exception=True flag, which means that you can override the style of validation error responses globally in your API. To do so, use a custom exception handler, as described above. By default this exception results in a response with the HTTP status code "400 Bad Request".

How do you increase validation error in DRF?

The easiest way to change the error style through all the view in your application is to always use serializer. is_valid(raise_exception=True) , and then implement a custom exception handler that defines how the error response is created.


2 Answers

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 Sentry do monitor and resolve this errors, that may have security issues embedded in them.

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

If your objective is to use ajax to submit information I suggest setting a form for 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.

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 Wikipedia and 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.

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 catch blocks 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_view you should have the test_method as in the third example.

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

By the way, you can see the Python Docs on User-defined Exceptions so 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.

like image 114
NBajanca Avatar answered Oct 14 '22 16:10

NBajanca


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.

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.

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.

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)
like image 22
kreld Avatar answered Oct 14 '22 16:10

kreld