28π
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).
9π
I might be missing something here but Iβm pretty sure you donβt need to define a custom parser in this caseβ¦
You can just use the JSONParser from DRF itself:
from rest_framework.decorators import api_view
from rest_framework.decorators import parser_classes
from rest_framework.parsers import JSONParser
@api_view(['POST'])
@parser_classes((JSONParser,))
def example_view(request, format=None):
"""
A view that can accept POST requests with JSON content.
"""
return Response({'received data': request.data})
- [Django]-Celery discover tasks in files with other filenames
- [Django]-Django: Using F arguments in datetime.timedelta inside a query
- [Django]-Filtering using viewsets in django rest framework
4π
Its been a while since this question is asked, so Iβm not sure if theres some differences with the framework at the time, but if anyone is searching for accessing the raw request body with recent versions, from the DRF docs on the parsers:
The set of valid parsers for a view is always defined as a list of classes. When request.data is accessed, REST framework will examine the Content-Type header on the incoming request, and determine which parser to use to parse the request content.
Meaning the parser is executed lazily when request.data
is accessed. So the solutions can be quite simply to read the request.body
, and cache it somewhere before accessing request.data
. No need to write a custom parser.
def some_action(self, request):
raw_body = request.body
parsed_body = request.data['something']
verify_signature(raw_body, request.data['key_or_something'])
- [Django]-Django/DRF β 405 Method not allowed on DELETE operation
- [Django]-Add rich text format functionality to django TextField
- [Django]-Django get objects not referenced by foreign key
0π
Updated solution with custom parser:
site_root/common/parsers.py
import codecs
from django.conf import settings
from rest_framework.exceptions import ParseError
from rest_framework import renderers
from rest_framework.parsers import BaseParser
from rest_framework.settings import api_settings
from rest_framework.utils import json
class BodySavingJSONParser(BaseParser):
"""
Parses JSON-serialized data.
"""
media_type = 'application/json'
renderer_class = renderers.JSONRenderer
strict = api_settings.STRICT_JSON
def parse(self, stream, media_type=None, parser_context=None):
"""
Parses the incoming bytestream as JSON and returns the resulting data.
"""
parser_context = parser_context or {}
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
request = parser_context.get('request')
try:
decoded_stream = codecs.getreader(encoding)(stream)
decoded_content = decoded_stream.read()
# Saving decoded request original body to original_body
setattr(request, 'original_body', decoded_content)
parse_constant = json.strict_constant if self.strict else None
return json.loads(decoded_content, parse_constant=parse_constant)
except ValueError as exc:
raise ParseError('JSON parse error - %s' % str(exc))
site_root/myapp/views.py
from rest_framework.decorators import api_view, parser_classes
from rest_framework.response import Response
from common.parsers import BodySavingJSONParser
@api_view(['POST'])
@parser_classes([BodySavingJSONParser])
def view1(request, *args, **kwargs):
print(request.original_body)
return Response({})
- [Django]-Saving ModelForm error(User_Message could not be created because the data didn't validate)
- [Django]-Are sessions needed for python-social-auth
- [Django]-Troubleshooting Site Slowness on a Nginx + Gunicorn + Django Stack