我在 DRFs GitHub 上发现了有趣的 topic,但它并没有完全解决问题。我对此案进行了调查,并提出了一个巧妙的解决方案。令人惊讶的是SO上没有这样的问题,所以我决定在SO self-answer guidelines之后将其添加为公众。
了解问题和解决方案的关键是HttpRequest.body (source) 的工作原理:
@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
访问body 时 - 如果self._body 已设置,则直接返回,否则将读取内部请求流并将其分配给_body:self._body = self.read()。从那时起,对body 的任何进一步访问都会退回到return self._body。此外,在读取内部请求流之前,还有一个if self._read_started 检查,如果“读取已开始”,则会引发异常。
self._read_started 标志正在由 read() 方法 (source) 设置:
def read(self, *args, **kwargs):
self._read_started = True
try:
return self._stream.read(*args, **kwargs)
except IOError as e:
six.reraise(UnreadablePostError, ...)
现在应该清楚的是,如果只调用了read() 方法而不将其结果分配给请求self._body,则在访问request.body 后将引发RawPostDataException。
现在让我们看看 DRF JSONParser 类 (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))
(我选择了稍旧的版本 o DRF 源,因为在 2017 年 5 月之后出现了一些性能改进,这掩盖了理解我们问题的关键线)
现在应该清楚stream.read() 调用设置了_read_started 标志,因此body 属性不可能再次访问流(在解析器之后)。
解决办法
“无 request.body”方法是 DRF 的意图(我猜),因此尽管在技术上可以启用对 request.body 的全局访问(通过自定义中间件) - 如果不深入了解其所有内容,则不应这样做后果。
request.body 属性的访问权限可以通过以下方式在本地显式授予:
你需要定义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))
那么就可以在需要访问原始请求内容的时候使用了:
@api_view(['POST'])
@parser_classes((MyJSONParser,))
def example_view(request, format=None):
return Response({'received data': request.raw_body})
虽然request.body 仍然无法在全球范围内访问(正如 DRF 作者的意图)。