title: Modify DRF Request objects for dispatch to other views
body: <h1>Context</h1>
  
  <p>AWS Elastic Beanstalk Worker tiers can be configured to accept a <em>single</em> AWS SQS queue automatically. Incoming messages on this queue can be routed to a <em>single</em> endpoint which some instance in the worker tier will respond to; they show up to the workers as POST events.</p>
  
  <p>The software my workers are running is based on Django/DRF.</p>
  
  <h1>Goal</h1>
  
  <p>I want my worker tier instances to be able to handle more than one type of incoming event. Ideally, SQS could be configured to deliver the requests to more than one endpoint, but that appears to be impossible.</p>
  
  <h1>My implementation</h1>
  
  <pre><code>class DispatchSerializer(serializers.Serializer):
      "`to` is the name of the destination endpoint; `payload` is the data I want delivered"
      to = serializers.CharField(
          max_length=80,
      )
  
      payload = serializers.JSONField()
  
  @api_view(http_method_names=['POST'])
  @permission_classes([AllowAny])
  def dispatch(request):
      """
      Dispatch a request to a sibling endpoint.
      """
      routing_table = {
          ... # this is specific to my application; 
              # it's just a dict of names to views
      }
  
      serializer = DispatchSerializer(data=request.data)
      if serializer.is_valid(raise_exception=True):
          to = serializer.validated_data['to']
          try:
              view = routing_table[to]
          except KeyError:
              raise Http404()
  
          # https://github.com/encode/django-rest-framework/blob/master/rest_framework/request.py#L183-L187
          request._full_data = serializer.validated_data['payload']
          return view(request)
  
      # If this returns other than 200, SQS simply re-attempts the request later. 
      # No returned data is preserved, so we might as well not return any.
      return Response()
  </code></pre>
  
  <p>As you can see, I attempt to simply replace <code>_full_data</code> attribute of the request with the payload, so that the inner view sees only the data intended for it (which to the dispatch view is the payload).</p>
  
  <h1>The problem</h1>
  
  <p>This implementation doesn't actually work. I get errors of this form:</p>
  
  <pre><code>Traceback (most recent call last):
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/request.py", line 378, in __getattribute__
      return super(Request, self).__getattribute__(attr)
  AttributeError: 'Request' object has no attribute 'body'
  
  During handling of the above exception, another exception occurred:
  
  Traceback (most recent call last):
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/request.py", line 378, in __getattribute__
      return super(Request, self).__getattribute__(attr)
  AttributeError: 'Request' object has no attribute 'body'
  
  During handling of the above exception, another exception occurred:
  
  Traceback (most recent call last):
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/django/core/handlers/exception.py", line 39, in inner
      response = get_response(request)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/django/core/handlers/base.py", line 187, in _get_response
      response = self.process_exception_by_middleware(e, request)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/django/core/handlers/base.py", line 185, in _get_response
      response = wrapped_callback(request, *callback_args, **callback_kwargs)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
      return view_func(*args, **kwargs)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/django/views/generic/base.py", line 68, in view
      return self.dispatch(request, *args, **kwargs)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/views.py", line 483, in dispatch
      response = self.handle_exception(exc)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/views.py", line 443, in handle_exception
      self.raise_uncaught_exception(exc)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/views.py", line 480, in dispatch
      response = handler(request, *args, **kwargs)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/decorators.py", line 52, in handler
      return func(*args, **kwargs)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/project/django-backend/cardamom/worker/views.py", line 196, in dispatch
      return view(request)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
      return view_func(*args, **kwargs)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/django/views/generic/base.py", line 68, in view
      return self.dispatch(request, *args, **kwargs)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/views.py", line 483, in dispatch
      response = self.handle_exception(exc)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/views.py", line 443, in handle_exception
      self.raise_uncaught_exception(exc)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/views.py", line 480, in dispatch
      response = handler(request, *args, **kwargs)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/decorators.py", line 52, in handler
      return func(*args, **kwargs)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/project/django-backend/cardamom/core/views/mailchimp.py", line 229, in wrapper
      return func(*args, **kwargs)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/project/django-backend/cardamom/worker/views.py", line 52, in wrapper
      result = f(log_entry, *args, **kwargs)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/project/django-backend/cardamom/worker/views.py", line 106, in match_one
      user_id = int(request.data['user_id'])
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/request.py", line 378, in __getattribute__
      return super(Request, self).__getattribute__(attr)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/request.py", line 186, in data
      self._load_data_and_files()
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/request.py", line 246, in _load_data_and_files
      self._data, self._files = self._parse()
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/request.py", line 290, in _parse
      stream = self.stream
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/request.py", line 378, in __getattribute__
      return super(Request, self).__getattribute__(attr)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/request.py", line 173, in stream
      self._load_stream()
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/request.py", line 270, in _load_stream
      self._stream = six.BytesIO(self.body)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/request.py", line 382, in __getattribute__
      return getattr(self._request, attr)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/rest_framework/request.py", line 382, in __getattribute__
      return getattr(self._request, attr)
    File "/mnt/d/Users/coriolinus/Documents/Toptal/cardamom/.venv/lib/python3.4/site-packages/django/http/request.py", line 264, in body
      raise RawPostDataException("You cannot access body after reading from request's data stream")
  django.http.request.RawPostDataException: You cannot access body after reading from request's data stream
  </code></pre>
  
  <h1>Problem restatement</h1>
  
  <p>The <code>request</code> object is complicated; generating a new one is implausible. At the same time, we need to modify the data of this one in order to properly pass it along to the next view, so that view only sees the data in its expected format.</p>
  
  <p>How can I accomplish this?</p>
  
  <h1>[edit] Passing an un-modified request doesn't work either</h1>
  
  <p>I tried editing the above example to remove any modification of the <code>request</code> object, and instead created the following serializer mixin:</p>
  
  <pre><code>class UndispatcherMixin:
      """
      Adjust a serializer such that it can accept its normal serialization,
      or alternately the payload of a DispatchSerializer.
      """
  
      def is_valid(self, raise_exception=False):
          if not hasattr(self, '_validated_data'):
              assert hasattr(self, 'initial_data'), (
                  'Cannot call `.is_valid()` as no `data=` keyword argument was '
                  'passed when instantiating the serializer instance.'
              )
              ds = DispatchSerializer(data=self.initial_data)
  
              if ds.is_valid(raise_exception=False):
                  self.initial_data = ds.validated_data['payload']
          return super().is_valid(raise_exception=raise_exception)
  </code></pre>
  
  <p>This mixin was then added to the relevant serializers used by the downstream views. Interestingly, it failed with the same <code>django.http.RawPostDataException</code> as before.</p>
  
  <p>It's received wisdom all over SO that you can just call your views as normal functions, so long as you pass in a proper <code>request</code> object. This may not actually be true, at least for the case where the view was created by the DRF <code>@api_view</code> decorator. </p>
  
  <p>Still working on a way to actually solve this.</p>