【问题标题】:How to access request body when using Django Rest Framework and avoid getting RawPostDataException使用 Django Rest Framework 时如何访问请求正文并避免出现 RawPostDataException
【发布时间】:2018-05-19 15:18:57
【问题描述】:

我需要获取 POST 请求正文的原始内容(作为字符串),但当我尝试访问 request.body 时遇到异常:

django.http.request.RawPostDataException:
You cannot access body after reading from request's data stream

我知道在使用 Django Rest Framework 时建议使用 request.data 而不是 request.body,但为了验证数字签名,我必须将请求正文采用原始且“未触及”的形式,因为这是第 3 方签署的内容,也是我需要验证的内容。

伪代码:

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)

【问题讨论】:

    标签: python django rest django-rest-framework http-post


    【解决方案1】:

    问这个问题已经有一段时间了,所以我不确定当时的框架是否存在一些差异,但是如果有人正在搜索使用最新版本访问原始请求正文,来自DRF docs on the parsers

    视图的有效解析器集始终定义为类列表。当访问 request.data 时,REST framework 将检查传入请求的 Content-Type 标头,并确定使用哪个解析器来解析请求内容。

    意味着当访问request.data 时解析器会延迟执行。所以解决方案可以很简单地读取request.body,并在访问request.data之前将其缓存在某处。无需编写自定义解析器。

    def some_action(self, request):
      raw_body = request.body
      parsed_body = request.data['something']
      verify_signature(raw_body, request.data['key_or_something'])
    

    【讨论】:

      【解决方案2】:

      我在 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 作者的意图)。

      【讨论】:

        【解决方案3】:

        我可能在这里遗漏了一些东西,但我很确定在这种情况下您不需要定义自定义解析器......

        您可以只使用 DRF 本身的 JSONParser:

            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})
        

        【讨论】:

        • 是的,在基于类的视图中,您可以使用self.context['request'].data 来处理它。花费大量时间尝试以公认解决方案的样式修改 MultiPartParser。一行就搞定了。
        • 请注意,就我而言,我不想访问request.data,而是访问原始request.body。请求 data 返回带有解析 json 的 python dict。而对于非常签名,我必须将原始请求正文作为字符串检索。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-11-21
        • 2021-01-19
        • 1970-01-01
        • 1970-01-01
        • 2018-11-05
        • 1970-01-01
        相关资源
        最近更新 更多