Django REST framework Exception Handling

  • 2021-11-13 08:36:48
  • OfStack

The catalogue is written at the front
DRF Exception Handling
1. Common DRF anomalies
2. Custom exceptions
3. Use custom exceptions
4. Verify the results
Advanced Exception Handling
1. Modify custom exceptions
2. Customize more exceptions
3. Add a test interface
4. Verify the results
Summarize
References

Write at the front

In the past two days, I have been thinking about the basic knowledge about DRF that is necessary for the project and has not been mentioned yet. This is not yesterday to write the log related functions directly thought of the exception handling related functions, in fact, in the previous project in the early stage is no unified 1 exception capture means. It may be that DRF's own exceptions can meet most functions, or it may be lazy, so it uses a rough way to throw exceptions in the form of status code 500, and then you can see all exception information in the log. To do so, the code is actually not robust enough, and the inexplicable 500 of the front end is not friendly enough, so today I will add 1 exception-related knowledge.

DRF Exception Handling

1. Common exceptions to DRF

AuthenticationFailed/NotAuthenticated 1 The exception status code is "401 Unauthenticated", which will be returned when there is no login authentication, and can be used when customizing login. PermissionDenied 1 is generally used for authentication, and the status code 1 is "403 Forbidden". The general status code of ValidationError 1 is "400 Bad Request", which is mainly for field verification in serializers, such as field type verification, field length verification and custom field format verification.

2. Custom exceptions

The main idea of defining exceptions here comes from ValidationError, the format of exception return, which is convenient for front-end system 1 to handle similar exceptions.

Custom exception


#  New  utils/custom_exception.py

class CustomException(Exception):
    _default_code = 400

    def __init__(
        self,
        message: str = "",
        status_code=status.HTTP_400_BAD_REQUEST,
        data=None,
        code: int = _default_code,
    ):

        self.code = code
        self.status = status_code
        self.message = message
        if data is None:
            self.data = {"detail": message}
        else:
            self.data = data

    def __str__(self):
        return self.message

Custom exception handling


# utils/custom_exception.py
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.
    
    #  Here, the custom  CustomException  Return directly to ensure that other system exceptions are not affected 
    if isinstance(exc, CustomException):
        return Response(data=exc.data, status=exc.status)
    response = exception_handler(exc, context)
    return response

Configuring Custom Exception Handling Classes


REST_FRAMEWORK = {
    # ...
    "EXCEPTION_HANDLER": "utils.custom_exception.custom_exception_handler",
}

3. Use custom exceptions

Use the interface in the previous article to test the handling of custom exceptions


class ArticleViewSet(viewsets.ModelViewSet):
    """
     That allows users to view or edit API Path. 
    """
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

    @action(detail=False, methods=["get"], url_name="exception", url_path="exception")
    def exception(self, request, *args, **kwargs):
        #  Log usage  demo
        logger.error(" Custom exception ")
        raise CustomException(data={"detail": " Custom exception "})

4. Verify the results


$ curl -H 'Accept: application/json; indent=4' -u admin:admin http://127.0.0.1:8000/api/article/exception/
{
    "detail": " Custom exception "
}

Advanced Exception Handling

Although the above code can meet 90% of the requirements, the wrong definition is too general. It is difficult to define management errors centrally. Compared with custom exceptions in common projects, it has the advantage of flexibility. However, with more and more exceptions thrown in the code and scattered in every corner, it is not conducive to update and maintenance. Therefore, the following code under Modification 1 has the definition of Unified 1 for exceptions, and also supports custom return of HTTP status codes.

1. Modify custom exceptions


# utils/custom_exception.py

class CustomException(Exception):
    #  Customize code
    default_code = 400
    #  Customize  message
    default_message = None

    def __init__(
            self,
            status_code=status.HTTP_400_BAD_REQUEST,
            code: int = None,
            message: str = None,
            data=None,
    ):
        self.status = status_code
        self.code = self.default_code if code is None else code
        self.message = self.default_message if message is None else message

        if data is None:
            self.data = {"detail": self.message, "code": self.code}
        else:
            self.data = data

    def __str__(self):
        return str(self.code) + self.message

2. Customize more exceptions


class ExecuteError(CustomException):
    """ Error in execution """
    default_code = 500
    default_message = " Error in execution "


class UnKnowError(CustomException):
    """ Error in execution """
    default_code = 500
    default_message = " Unknown error "

3. Add a test interface


class ArticleViewSet(viewsets.ModelViewSet):
    """
     That allows users to view or edit API Path. 
    """
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

    @action(detail=False, methods=["get"], url_name="exception", url_path="exception")
    def exception(self, request, *args, **kwargs):
        #  Log usage  demo
        logger.error(" Custom exception ")
        raise CustomException(data={"detail": " Custom exception "})

    @action(detail=False, methods=["get"], url_name="unknown", url_path="unknown")
    def unknown(self, request, *args, **kwargs):
        #  Log usage  demo
        logger.error(" Unknown error ")
        raise UnknownError()

    @action(detail=False, methods=["get"], url_name="execute", url_path="execute")
    def execute(self, request, *args, **kwargs):
        #  Log usage  demo
        logger.error(" Execution error ")
        raise ExecuteError()

4. Verify the results


curl -H 'Accept: application/json; indent=4' -u admin:admin http://127.0.0.1:8000/api/article/unknown/
{
    "detail": " Unknown error ",
    "code": 500
}
$ curl -H 'Accept: application/json; indent=4' -u admin:admin http://127.0.0.1:8000/api/article/execute/
{
    "detail": " Error in execution ",
    "code": 500
}

Summarize

It should be noted that the custom exception handling function needs to continue to execute rest_framework. views.exception_handler after handling the custom exception, because the execution here still needs to be compatible with the existing exception handling; The exception handling logic related to DRF is posted below.

This handler handles APIException and Http404 PermissionDenied inside Django by default. Other exceptions return None, triggering DRF 500 errors.


def exception_handler(exc, context):
    """
    Returns the response that should be used for any given exception.

    By default we handle the REST framework `APIException`, and also
    Django's built-in `Http404` and `PermissionDenied` exceptions.

    Any unhandled exceptions may return `None`, which will cause a 500 error
    to be raised.
    """
    if isinstance(exc, Http404):
        exc = exceptions.NotFound()
    elif isinstance(exc, PermissionDenied):
        exc = exceptions.PermissionDenied()

    if isinstance(exc, exceptions.APIException):
        headers = {}
        if getattr(exc, 'auth_header', None):
            headers['WWW-Authenticate'] = exc.auth_header
        if getattr(exc, 'wait', None):
            headers['Retry-After'] = '%d' % exc.wait

        if isinstance(exc.detail, (list, dict)):
            data = exc.detail
        else:
            data = {'detail': exc.detail}

        set_rollback()
        return Response(data, status=exc.status_code, headers=headers)

    return None

References

Django REST framework Exception Documentation
Django Exception Documentation


Related articles: