Search code examples
djangodjango-rest-frameworkdispatchdjango-drf-renderer

Django Rest Framework: Return response from mixin's dispatch method


In order to interact with slack, a server needs to be able to validate requests based on some cryptographic hashing. If this check returns false, the server should respond with a 400. It seems sensible to do this as a mixin:

class SlackValidationMixin:
    def dispatch(self, request, *args, **kwargs):
        if validate_slack_request(request):
            return super().dispatch(request, *args, **kwargs)
        else:
            return Response(status=status.HTTP_400_BAD_REQUEST)

This gives the error "accepted_renderer not set on Response" Based on a SO question, I added the following:

class SlackValidationMixin:
    def dispatch(self, request, *args, **kwargs):
        if validate_slack_request(request):
            return super().dispatch(request, *args, **kwargs)
        else:
            response = Response(status=status.HTTP_400_BAD_REQUEST)
            response.accepted_renderer = JSONRenderer
            response.accepted_media_type = "application/json"
            response.renderer_context = {}
            return response

But this gives the error: AttributeError: 'NoneType' object has no attribute 'get_indent'

Why does it need an accepted_renderer, given that it is only responding with an HTTP status code, with no additional data? What is the easiest way of getting around this?

Following suggestion in answer to make EmptyResponse object inheriting from Response:

Traceback (most recent call last):
  File "path/lib/python3.8/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "path/lib/python3.8/site-packages/django/utils/deprecation.py", line 96, in __call__
    response = self.process_response(request, response)
  File "path/lib/python3.8/site-packages/django/middleware/common.py", line 106, in process_response
    if response.status_code == 404:
AttributeError: 'dict' object has no attribute 'status_code'

Solution

  • At first the solution: your second approach is fine, you only need to instantiate the JSONResponse class (DRF does this in the get_renderers method of views.APIView):

    response.accepted_renderer = JSONRenderer()
    

    Background:

    • Django WSGIHandler (inherited from Basehandler) calls response.render() to render the response
    • DRF Response (inherited from SimpleTemplateResponse) object has a render method that gets the rendered content via the rendered_content property (which calls the render method of the renderer with the passed data, media type and context)
    • In the initial content-negotiation stage, the renderer is set according to the DEFAULT_RENDERER_CLASSES/APIView.renderer_classes setting and the Aceept header passed by client; the selected renderer is set in the HttpRequest object as accepted_renderer and the media type as request.accepted_media_type attributes
    • If the renderer needs any extra context, the Response object also needs the renderer_context attribute; for example, views.APIView sets the current view, request, and arguments as renderer_context dict

    Now it should be clear why you need the attributes with Response object -- to get the renderer, media type and to pass any extra context that might be needed by the selected renderer.


    You've added an answer, where you're setting the above mentioned attributes and then from the renderer returning an empty dict as response. If you want to follow that route, a much easier and cleaner option would be to create a subclass of Response and return an empty dict from the render method e.g.:

    class EmptyResponse(rest_framework.response.Response):
    
         def render(self):
             # You can have your own rendered content here
             self.content = b''
             return self
    

    Now only returning the EmptyResponse object would do, no need to add the renderer related attributes:

    class SlackValidationMixin:
    
        def dispatch(self, request, *args, **kwargs):
            if validate_slack_request(request):
                return super().dispatch(request, *args, **kwargs)
            else:
                return EmptyResponse(status=status.HTTP_400_BAD_REQUEST)
    

    Now, unless you're adding some custom content, the deferred rendering is not needed; you can directly return HttpResponse object:

    from django.http import HttpResponse
    
    class SlackValidationMixin:
    
        def dispatch(self, request, *args, **kwargs):
            if validate_slack_request(request):
                return super().dispatch(request, *args, **kwargs)
            else:
                return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
    

    And if you want, you can pass the content (as bytes) while initializing HttpResponse. But if for some reason, you need lazy rendering, you need to use Response.render.