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 processed
rest_framework.views.exception_handler
Because 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