Tidbits | Feb. 4, 2020

Custom Exceptions in Django REST Framework

by Lacey Williams Henschel |   More posts by Lacey

I was working on a project with a coworker recently and I noticed in one of their pull requests that they used a custom exception in one of our Django REST Framework viewsets. I prefer this way to what I was doing before, so I wanted to share it with you!

My Less Clean, Less Consistent Exception Handling

I made good use of the status codes page of the DRF docs. For example, if a user uploaded a CSV file that my view couldn’t read, I might have had something like this:

from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView


class MyCSVUploadView(APIView):
    def post(self, request, *args, **kwargs):
        
        if file_error:
            return Response(
                {"detail": "Unable to read file."}, 
                status=status.HTTP_400_BAD_REQUEST
            )

This works just fine. But in some cases, I might return the same type of error response in multiple places. For a university that is no longer accepting applications, I might have multiple places where I am essentially passing the same “Deadline passed” message. Although the response is just one line of code, I have to find it, copy it, and paste it to make sure I’m using the same wording, or deal with inconsistency in my exception messages.

Custom Exception Classes for Clean Consistency

For exception conditions that you encounter frequently in your code, you can override DRF’s APIException class and create your own exceptions! Read more in the docs.

Import the APIException class and create a new class that inherits from it. Then set the status_code, default_detail, and default_code attributes.

# exceptions.py
from rest_framework.exceptions import APIException


class UnreadableCSVFile(APIException):
    status_code = 400
    default_detail = "Unable to read file."
    default_code = "unreadable_csv_file"

The status_code attribute defines the HTTP status code you want to send in the response with this exception; refer to the DRF list of status codes for help. The default_detail attribute is the human-readable exception message you want to pass in the response. The default_code is a string that represents this specific exception.

In your view, instead of manually returning a Response() object, raise your custom exception.

# views.py
from rest_framework.views import APIView

From my_app.exceptions import UnreadableCSVFile

class MyCSVUploadView(APIView):
    def post(self, request, *args, **kwargs):
        
        if file_error:
            raise UnreadableCSVFile()

This allows my exceptions to be reusable across my app, which means the language I use in exception messages is always consistent.

Custom Exception Handlers

You can further customize your exceptions by creating a custom handler function (docs on custom exception handling). This is especially helpful if there are pieces of data, like the status code, that you want to ensure are included in all of your HTTP responses. This is a slightly different goal than what I refer to above, but it’s worth knowing about. Here is the example from the docs:

from rest_framework.views import exception_handler

def custom_exception_handler(exc, context):
    # Call REST framework's default exception handler first, 
    # to get the standard error response.
    response = exception_handler(exc, context)

    # Now add the HTTP status code to the response.
    if response is not None:
        response.data['status_code'] = response.status_code

    return response

This adds the status_code to each exception response sent in your app.

You would then add your custom exception handler to the REST_FRAMEWORK settings in your settings.py so this custom exception handler will be executed every time there is an API exception:

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}

For my example above, with the unreadable CSV file, the response would look like this:

{"status_code": 400, "detail": "Unable to read file."}

With the status code returned as part of the response dictionary.

Thanks to Frank Wiles for teaching me about custom exceptions, and Jeff Triplett for reviewing a draft of this article.

I was working on a project with a coworker recently and I noticed in one of their pull requests that they used a custom exception in one of our Django REST Framework viewsets. I prefer this way to what I was doing before, so I wanted to share it with you!{% else %}

2020-02-04T17:06:00 2020-02-04T18:15:43.501545 2020