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
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)
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
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_required
or a permission required django will respond with the appropriate code without you doing anything).
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.
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.
(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 BusinessLogicViolation
exception with a different message in my_business_logic_is_violated()
according to the place where it was generated.