Django REST framework Exception Handling
- 2021-11-13 08:36:48
- OfStack
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