2

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'
alex_halford
  • 369
  • 3
  • 10
  • Why don't you try to return the response from corresponding method? linke 'post()` or `create()` or something similar? – JPG Dec 02 '19 at 13:50
  • Well, I could, and I could scrap the mixin altogether, but I'd be doing this check for all methods, so it seems like it should be done in a mixin, rather than repeated across every method. – alex_halford Dec 02 '19 at 14:00
  • Yeah, that's great. But do we send the *cryptographic* data to all endpoints? say GET and POST of `/foo-bar/secret/` ? – JPG Dec 02 '19 at 14:06
  • Look, I can get around this issue myself, I know. This question is specifically about overriding the dispatch method, using a mixin. Do you have any thoughts on that particular question? – alex_halford Dec 02 '19 at 14:07
  • don't be offended man. IMHO, you are doing it in the wrong way, at least for the specified scenario. – JPG Dec 02 '19 at 14:11
  • Not offended, just asking a specific question about mixins. Granted, this verification is not done on get requests, but might be done on POST,DELETE,PUT etc. It seems as though mixins are designed for this exact purpose. In any case, I've found a workaround. – alex_halford Dec 02 '19 at 14:14
  • @alex_halford I'm actually looking for how you implemented `validate_slack_request(request)` Do you have a link to a good example? – Matt Jun 11 '20 at 18:47

2 Answers2

6

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.

heemayl
  • 39,294
  • 7
  • 70
  • 76
  • Instantiating the JSONRenderer works, but the approach of subclassing the response doesn't: `AttributeError: 'dict' object has no attribute 'status_code'` – alex_halford Dec 02 '19 at 16:10
  • @alex_halford Have you inherited from `rest_framework.response.Response` ? I need to see the full error message. – heemayl Dec 02 '19 at 16:20
  • It was a submodule import: from rest_framework.response import Response, and then inheriting from Response, but yes, it does inherit. Traceback in question – alex_halford Dec 02 '19 at 17:04
  • @alex_halford Could you please add your complete code to the question: both `EmptyResponse` and `SlackValidationMixin` ? – heemayl Dec 02 '19 at 17:11
  • It is identical to what you posted. The only difference is the import as mentioned above, but that won't make any difference – alex_halford Dec 02 '19 at 17:12
  • @alex_halford Fixed. Thanks for reporting. – heemayl Dec 02 '19 at 17:21
0

Creating a renderer that returns nothing seems to get this to work. I would be surprised if this were the 'correct' way, but it gets the job done.

class NoneRenderer(BaseRenderer):
    def render(self, *args, **kwargs):
        return {}


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 = NoneRenderer
            response.accepted_media_type = "*/*"
            response.renderer_context = {}
            return response
alex_halford
  • 369
  • 3
  • 10