How to access request body when using Django Rest Framework and avoid getting RawPostDataException

django, django-rest-framework, http-post, python, rest

Solution

I have found interesting topic on DRFs GitHub, but it does not fully cover the problem. I have investigated the case and came up with a neat solution. Surprisingly there was no such question on SO, so I decided to add it for public following the SO self-answer guidelines.

The key for understanding the problem and solution is how the `HttpRequest.body` (source) works:

@property
def body(self):
    if not hasattr(self, '_body'):
        if self._read_started:
            raise RawPostDataException("You cannot access body after reading from request's data stream")
        # (...)
        try:
            self._body = self.read()
        except IOError as e:
            raise UnreadablePostError(*e.args) from e
        self._stream = BytesIO(self._body)
    return self._body

When accessing `body` - if the `self._body` is already set its simply returned, otherwise the internal request stream is being read and assigned to _body: `self._body = self.read()`. Since then any further access to `body` falls back to `return self._body`. In addition before reading the internal request stream there is a `if self._read_started` check which raises an exception if "read has started".

The `self._read_started` flague is being set by the `read()` method (source):

def read(self, *args, **kwargs):
    self._read_started = True
    try:
        return self._stream.read(*args, **kwargs)
    except IOError as e:
        six.reraise(UnreadablePostError, ...)

Now it should be clear that the `RawPostDataException` will be raised after accessing the `request.body` if only the `read()` method has been called without assigning its result to requests `self._body`.

Now lets have a look at DRF `JSONParser` class (source):

class JSONParser(BaseParser):
    media_type = 'application/json'
    renderer_class = renderers.JSONRenderer

    def parse(self, stream, media_type=None, parser_context=None):
        parser_context = parser_context or {}
        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
        try:
            data = stream.read().decode(encoding)
            return json.loads(data)
        except ValueError as exc:
            raise ParseError('JSON parse error - %s' % six.text_type(exc))

(I have chosen slightly older version o DRF source, cause after May 2017 there have been some performance improvements that obscure the key line for understanding our problem)

Now it should be clear that the `stream.read()` call sets the `_read_started` flague and therefore it is impossible for the `body` property to access the stream once again (after the parser).

The solution

The "no request.body" approach is a DRF intention (I guess) so despite it is technically possible to enable access to `request.body` globally (via custom middleware) - it should NOT be done without deep understanding of all its consequences.

The access to the `request.body` property may be explicitly and locally granted in the following manner:

You need to define custom parser:

import json
from django.conf import settings
from rest_framework.exceptions import ParseError
from rest_framework import renderers
from rest_framework.parsers import BaseParser

class MyJSONParser(BaseParser):
    media_type = 'application/json'
    renderer_class = renderers.JSONRenderer

    def parse(self, stream, media_type=None, parser_context=None):
        parser_context = parser_context or {}
        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
        request = parser_context.get('request')
        try:
            data = stream.read().decode(encoding)
            setattr(request, 'raw_body', data) # setting a 'body' alike custom attr with raw POST content
            return json.loads(data)
        except ValueError as exc:
            raise ParseError('JSON parse error - %s' % six.text_type(exc))

Then it can be used when it is necessary to access raw request content:

@api_view(['POST'])
@parser_classes((MyJSONParser,))
def example_view(request, format=None):
    return Response({'received data': request.raw_body})

While `request.body` still remains globally inaccessible (as DRF authors intended).

Problem

I need to get the raw content of POST request body (as a string) yet when I try to access `request.body` I'm getting an exception: ``` django.http.request.RawPostDataException: You cannot access body after reading from request's data stream ``` I am aware that it is adviced to use `request.data` instead of `request.body` when using Django Rest Framework, yet for the purpose of validating digital signature I have to have the request body in a raw and "untouched" form, since this is what 3rd-party signed and what I need to validate. Pseudocode: ``` 3rd_party_sign(json_data + secret_key) != validate_sign(json.dumps(request.data) + secret_key) 3rd_party_sign(json_data + secret_key) == validate_sign(request.body + secret_key) ```

Original source