APIview's request life cycle source code analysis

APIview's request life cycle source code analysis

APIview's request life cycle source code analysis

Start the Django project => load the settings file => load the models, views, and urls files, execute the urls file, and call the as_view() method of the view class.

The as_view() method of APIview inherits the as_view() method of the parent class, and adds the function of partially disabling the csrf middleware

 def as_view(cls, **initkwargs):
        """
        Store the original class on the view function.

        This allows us to discover information about the view when we do URL
        reverse lookups. Used for breadcrumb generation.
        """
        if isinstance(getattr(cls,'queryset', None), models.query.QuerySet):
            def force_evaluation():
                raise RuntimeError(
                    'Do not evaluate the `.queryset` attribute directly, '
                    'as the result will be cached and reused between requests. '
                    'Use `.all()` or call `.get_queryset()` instead.'
                )
            cls.queryset._fetch_all = force_evaluation
        #Assign the parent class as_view to your own view space, and add its class name and parameters to the properties of your own class
        view = super().as_view(**initkwargs)
        view.cls = cls
        view.initkwargs = initkwargs

        # Note: session based authentication is explicitly CSRF validated,
        # all other authentication is CSRF exempt.
        
        #
        #Partially disable csrf authentication
        return csrf_exempt(view)

The parent class of APIview is Django's view class view. as_view() inherits the as_view function of the parent class and calls the dispatch method to distribute requests. The following is the as_view() of the parent class

 @classonlymethod
    def as_view(cls, **initkwargs):
        """
        Main entry point for a request-response process.
        """
        '''Omit the code'''
       .................................................. ......
        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self,'get') and not hasattr(self,'head'):
                self.head = self.get
            self.request = request
            self.args = args
            self.kwargs = kwargs
            #Distribution request
            return self.dispatch(request, *args, **kwargs)
        # Here self is the object instantiated by the view class, make no mistake
        view.view_class = cls
        view.view_initkwargs = initkwargs

        # take name and docstring from class
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        update_wrapper(view, cls.dispatch, assigned=())
        return view

The object instantiated by the view class finds the dispatch method in its own name space. If not, find the dispatch method in the base class APIview. The dispatch method of APIview is an overwrite of the dispatch method of the view class, and the dispatch method of the view class is optimized. Let's look at the source code of APIview's dispatch method with specific optimization:

 def dispatch(self, request, *args, **kwargs):
        """
        `.dispatch()` is pretty much the same as Django's regular dispatch,
        but with extra hooks for startup, finalize, and exception handling.
        """
        self.args = args
        self.kwargs = kwargs
        # initialize_request re-encapsulates the request object and parses the content of the request object
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers # deprecate?

        try:
            #3.major certifications (authentication, authority, frequency), used to replace csrf security certification, which is much stronger than csrf certification
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
                #If there is a corresponding method in the request of wsgi, use wsgi, if not, use your own, commonly used methods in the source code
            else:
                handler = self.http_method_not_allowed
                #If you don't have it, report http_method_not_allowed exception

            response = handler(request, *args, **kwargs)

        except Exception as exc:
            #Exception handling module, handling abnormal branches
            response = self.handle_exception(exc)
        #Secondary package response, processing the content of the response, and rendering the results of the processing
        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response

Summarize the functions completed in the rewritten dispatch:

1. Encapsulate the request object twice and parse the content of the request object

2. Call the function initial to perform three major authentications on the request, and capture exceptions in the process

3. Execute authenticated custom requests such as get, post, patch, delete, etc. through reflection

4. If there is an exception during the execution of steps 2 and 3 above, call the handle_exception method to handle the caught exception.

5. Process the content of the response through the finalize_response method, and whether to render the content

The above is the request process of the Django rest framework source code. Let's take a rough look at the source code of the request module, parsing module, corresponding module, exception handling module, and rendering module.

Request module

The general functions of the request module are as follows:

1. Convert the request object of wsgi into an object of the request class of drf

2. The encapsulated request object is fully compatible with wsgi's request object, and the original request object is saved in the new request._request

3. Reformatting the storage location of the requested data

Splicing parameters: request.query_params

Data packet parameters: request.data

# Source code analysis:
# Entry: request of the dispatch method of APIVIew=self.initialize_request(request, *args, **kwargs)
# print(request._request.method) # Assign WSGI request to request._request internally

class Request:
    """
    Wrapper allowing to enhance a standard `HttpRequest` instance.

    Kwargs:
        -request(HttpRequest). The original request instance.
        -parsers_classes(list/tuple). The parsers to use for parsing the
          request content.
        -authentication_classes(list/tuple). The authentications used to try
          authenticating the request's user.
    """

    def __init__(self, request, parsers=None, authenticators=None,
                 negotiator=None, parser_context=None):
        assert isinstance(request, HttpRequest), (
            'The `request` argument must be an instance of '
            '`django.http.HttpRequest`, not `{}.{}`.'
            .format(request.__class__.__module__, request.__class__.__name__)
            #Store all the name space of the request class of wsgi into your own name space to achieve full compatibility with the request of wsgi.
        )

        self._request = request
        #Store the request of the parent class in our own _request, so that we can use the attributes and methods of the object point attributes and methods of the wsgi request, or directly manipulate the wsgi request object through the object point _request.
        self.parsers = parsers or ()
        self.authenticators = authenticators or ()
        self.negotiator = negotiator or self._default_negotiator()
        self.parser_context = parser_context
        self._data = Empty
        self._files = Empty
        self._full_data = Empty
        self._content_type = Empty
        self._stream = Empty

        if self.parser_context is None:
            self.parser_context = {}
        self.parser_context['request'] = self
        self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET

        force_user = getattr(request,'_force_auth_user', None)
        force_token = getattr(request,'_force_auth_token', None)
        if force_user is not None or force_token is not None:
            forced_auth = ForcedAuthentication(force_user, force_token)
            self.authenticators = (forced_auth,)

    def _default_negotiator(self):
        return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS()

    @property
    def content_type(self):
        meta = self._request.META
        return meta.get('CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE',''))
    
     @property
    def query_params(self):
        """
        More semantically correct name for request.GET.
        """
        #Rename the _request.GET attribute to query_params
        return self._request.GET

    @property
    def data(self):
        if not _hasattr(self,'_full_data'):
            self._load_data_and_files()
            #Rename _full_data
        return self._full_data
    
    
     def __getattr__(self, attr):
        """
        If an attribute does not exist on this instance, then we also attempt
        to proxy it to the underlying HttpRequest object.
        """
        try:#Get the attributes and methods of _request through the method of __getattr__
            return getattr(self._request, attr)
        except AttributeError:
            return self.__getattribute__(attr)

Parsing module

The parsing module only processes data packet parameters

# Source code analysis:
# Entry: request of the dispatch method of APIVIew=self.initialize_request(request, *args, **kwargs)
# Get the parsing class: parsers=self.get_parsers(),
# Perform local global default configuration search order to search: return [parser() for parser in self.parser_classes]
    def get_parsers(self):
        """
        Instantiates and returns the list of parsers that this view can use.
        """
        return [parser() for parser in self.parser_classes]
    #We click on parser_classes to reach the self.parser_classes attribute,
class APIView(View):

    # The following policies may be set at either globally, or per-view.
    renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
    parser_classes = api_settings.DEFAULT_PARSER_CLASSES#Here you can see that the parser is configured in the api_settings configuration
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
    throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
    content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
    metadata_class = api_settings.DEFAULT_METADATA_CLASS
    versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
    
    #In the settings, you can see that the default parser configuration is as follows, that is, the data types supported by the default parser are form-data, urlencoded, json
    DEFAULTS = {
    # Base API policies
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    ],
    'DEFAULT_PARSER_CLASSES': [
        'rest_framework.parsers.JSONParser',#json
        'rest_framework.parsers.FormParser',#urlencoded
        'rest_framework.parsers.MultiPartParser'#file form-data
    ],
   #Here is the global configuration, we can customize the parser we use in the project's settings file

Global configuration resolver

When we start the project after the following configuration in the drf settings file, our own configuration will be used first.

REST_FRAMEWORK = {
    'DEFAULT_PARSER_CLASSES': [
        'rest_framework.parsers.JSONParser',#json
        'rest_framework.parsers.FormParser',#urlencoded
        'rest_framework.parsers.MultiPartParser'#file form-data
    ]
}

Local configuration parser

We can also directly import the parser into our own view class, and use the parser_classes in our class first.

from rest_framework.views import APIView
from rest_framework.parsers import JSONParser, FormParser, MultiPartParser
class Book(APIView):
    parser_classes = [JSONParser,FormParser,MultiPartParser]

In summary, we can know the search order of the parser configuration: local (class attribute of the view class) => global (drf configuration of the settings file) => default (drf default configuration)

Response module

 class Response(SimpleTemplateResponse):
    """
    An HttpResponse that allows its data to be rendered into
    arbitrary media types.
    """

    def __init__(self, data=None, status=None,
                 template_name=None, headers=None,
                 exception=False, content_type=None):
        """
        Alters the init arguments slightly.
        For example, drop'template_name', and instead use'data'.

        Setting'renderer' and'media_type' will typically be deferred,
        For example being set automatically by the `APIView`.
        """
        super().__init__(None, status=status)

        if isinstance(data, Serializer):
            msg = (
                'You passed a Serializer instance as data, but'
                'probably meant to pass serialized `.data` or '
                '`.error`. representation.'
            )
            raise AssertionError(msg)

        self.data = data
        self.template_name = template_name
        self.exception = exception
        self.content_type = content_type

        if headers:
            for name, value in headers.items():
                self[name] = value

    
    @property
    def status_text(self):
        """
        Returns reason text corresponding to our HTTP response status code.
        Provided for convenience.
        """
        return responses.get(self.status_code,'')

    def __getstate__(self):
        """
        Remove attributes from the response that shouldn't be cached.
        """
        state = super().__getstate__()
        for key in (
            'accepted_renderer','renderer_context','resolver_match',
            'client','request','json','wsgi_request'
        ):
            if key in state:
                del state[key]
        state['_closable_objects'] = []
        return state


# data: Response data
# status: response network status code
# -------------------------
# template_name: Before and after the drf is completed, the back-end will not be separated to return to the page, but data cannot be returned (understand)
# headers: response headers, generally not specified, default
# exception: general exception response, it will be set to True, the default is False (nothing is fine if it is not set)
# content_type: The default is application/json, no need to deal with

Exception handling module

 def dispatch(self, request, *args, **kwargs):
        '''
       ...
       '''
        try:
            #3.major certifications (authentication, authority, frequency), used to replace csrf security certification, which is much stronger than csrf certification
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
                #If there is a corresponding method in the request of wsgi, use wsgi, if not, use your own, commonly used methods in the source code
            else:
                handler = self.http_method_not_allowed
                #If you don't have it, report http_method_not_allowed exception

            response = handler(request, *args, **kwargs)

        except Exception as exc:
            #Exception handling module, handling abnormal branches
            response = self.handle_exception(exc)
        #Secondary package response, processing the content of the response, and rendering the results of the processing
        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response

It can be seen from the source code that the scope of exception capture has been introduced when the request enters the three major authentications, but the exception here will not be better for the client-side exception handling, and for the server-side exception, a wave of code will be returned to the front-end, we enter handle_exception, look at the code for exception handling.

 def handle_exception(self, exc):
        """
        Handle any exception that occurs, by returning an appropriate response,
        or re-raising the error.
        """
        #Handle the response header for authentication exception
        if isinstance(exc, (exceptions.NotAuthenticated,
                            exceptions.AuthenticationFailed)):
            # WWW-Authenticate header for 401 responses, else coerce to 403
            auth_header = self.get_authenticate_header(self.request)

            if auth_header:
                exc.auth_header = auth_header
            else:
                exc.status_code = status.HTTP_403_FORBIDDEN
        #Get exception handling function exception_handler
        exception_handler = self.get_exception_handler()

        context = self.get_exception_handler_context()
        
        #Provide additional parameters for exception handling, in fact, is to get the view object that throws the exception and the requested parameters, see the source code of get_exception_handler_context below
        response = exception_handler(exc, context)#Exception object, view object and requested parameters

        #The default exception_handler function only processes the response object formed by the client exception, the server exception is not processed, and None is returned
        if response is None:
            #When response is none, hand it over to Django middleware for processing
            self.raise_uncaught_exception(exc)

        response.exception = True
        return response
    
    
    #View Object and Requested Parameters
     def get_exception_handler_context(self):
        """
        Returns a dict that is passed through to EXCEPTION_HANDLER,
        as the `context` argument.
        """
        return {
            'view': self,
            'args': getattr(self,'args', ()),
            'kwargs': getattr(self,'kwargs', {}),
            'request': getattr(self,'request', None)
        }

Rewrite exception handling function

In order to customize the content of the exception thrown by the system when the server is abnormal, we need to rewrite the exception handling function. Steps:

1. Configure EXCEPTION_HANDLER in the drf configuration of settings to point to the custom exception_handler function

2. When an exception occurs in drf, the exception_handler function will be called back, carrying the exception object and exception related information, and the exception information return and the logging log of the exception information will be completed in the exception_handler function.

Configure in Django's settings file:

   REST_FRAMEWORK = {'EXCEPTION_HANDLER':'api.exception_handler.exception_handler'}

Rewrite exception_handler in the exception_handler file

# Be sure to configure your own exception handling function for the exception module in the settings file
from rest_framework.views import exception_handler as drf_exception_handler
from rest_framework.response import Response

# First hand in a drf to handle the client exception, if the response is None, it means that the server is abnormal, handle it yourself
# Finally, we must record the abnormal phenomenon in the log file
def exception_handler(exc, context):
    response = drf_exception_handler(exc, context)
    detail ='%s-%s-%s'% (context.get('view'), context.get('request').method, exc)
    if not response: # server error
        response = Response({'detail': detail})
    else:
        response.data = {'detail': detail}

    # Core: To record response.data.get('detail') information to the log file
    # logger.waring(response.data.get('detail'))

    return response

Rendering module

The import method of the rendering module in APIView renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES, its function is that the result of Postman request is json, and the result of the browser request is the rendered page. In actual projects, the application scene is not large and can be partially integrated like the parsing module. Global configuration.

Reference: https://cloud.tencent.com/developer/article/1560238 APIview's request life cycle source code analysis-Cloud + Community-Tencent Cloud