Writing in the front

These two days, I have been thinking about the basic knowledge about DRF which is necessary for the project and has not been mentioned yet. This is not written yesterday to log related functions directly think of the exception handling functions, in fact in the early project was not a uniform means of exception catching. Either the DRF has built-in exceptions that fit most of the functionality, or it is lazy and throws exceptions in a rude way, using status code 500, and then you can see all the exception information in the log. In this way, the code is not robust enough, and the front end is not friendly enough to invoke 500, so today I will supplement the knowledge about exceptions.

DRF exception handling

1. Common DRF exceptions

  • AuthenticationFailed/ NotAuthenticated Generally, the exception status code is “401 Unauthenticated”, which is returned when there is no login authentication. It can be used for custom login.

  • PermissionDenied Is used during authentication. The normal status code is 403 Forbidden.

  • The ValidationError status code is “400 Bad Request”. It is used in serializers to verify fields, such as field types, field length, and custom field formats.

2. Customize exceptions

The main idea for defining an exception here comes from ValidationError, to unify the format of the return of an exception so that the front end can handle similar exceptions uniformly.

  • 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
Copy the code
  • 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.
    
    # return the CustomException to ensure that other exceptions in the system are not affected
    if isinstance(exc, CustomException):
        return Response(data=exc.data, status=exc.status)
    response = exception_handler(exc, context)
    return response
Copy the code
  • Configure a custom exception handling class
REST_FRAMEWORK = {
    # ...
    "EXCEPTION_HANDLER": "utils.custom_exception.custom_exception_handler",
}
Copy the code

3. Use user-defined exceptions

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

class ArticleViewSet(viewsets.ModelViewSet) :
    API path that allows users to view or edit. "" "
    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 using Demo
        logger.error("Custom exceptions")
        raise CustomException(data={"detail": "Custom exceptions"})
Copy the code

4. Verify the result

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

Exception handling advanced

The above code satisfies 90% of the requirements, but the error definition is too general. It is difficult to centrally define management errors and has the advantage of flexibility over custom exceptions in common projects, but with more exceptions thrown in the code and scattered around the corners, it is not easy to update and maintain. So let’s change the code to have a uniform definition of exceptions, as well as support for custom HTTP status codes.

1. Modify user-defined exceptions

# utils/custom_exception.py

class CustomException(Exception) :
    # Custom code
    default_code = 400
    # Custom 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
Copy the code

2. Customize more exceptions

class ExecuteError(CustomException) :
    """ Execution error """
    default_code = 500
    default_message = "Execution error"


class UnKnowError(CustomException) :
    """ Execution error """
    default_code = 500
    default_message = "Unknown error"

Copy the code

3. Add test interfaces

class ArticleViewSet(viewsets.ModelViewSet) :
    API path that allows users to view or edit. "" "
    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 using Demo
        logger.error("Custom exceptions")
        raise CustomException(data={"detail": "Custom exceptions"})

    @action(detail=False, methods=["get"], url_name="unknown", url_path="unknown")
    def unknown(self, request, *args, **kwargs) :
        # Log using 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 using Demo
        logger.error("Execution error")
        raise ExecuteError()

Copy the code

4. Verify the result

curl -H 'Accept: application/json; indent=4' -u admin:admin http://127.0. 01.:8000/api/article/unknown/
{
    "detail": "Unknown error"."code": 500
}
$ curl -H 'Accept: application/json; indent=4' -u admin:admin http://127.0. 01.:8000/api/article/execute/
{
    "detail": "Execution error"."code": 500
}

Copy the code

conclusion

  • Note that the custom exception handler needs to continue executing after the custom exception is processedrest_framework.views.exception_handlerBecause the execution here still needs to be compatible with existing exception handling; Here is the drF-related exception handling logic.

By default, this handler handles APIException and Django’s internal Http404 PermissionDenied. Other exceptions will return None, which will trigger a DRF 500 error.


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

Copy the code

The resources

  • Django REST Framework exception documentation
  • Django Exception Documentation