Repository: getsentry/sentry-python

Issue:
Make sure everything works with Django-Rest-Framework
We should django-rest-framework's `request.data` instead of trying to extract a structured body ourselves


Discussion:
``` bash
RawPostDataException
You cannot access body after reading from request's data stream
```

Way how you read request body kill Django-Stripe integration
https://github.com/dj-stripe/dj-stripe/blob/3eafe287ac93d94f2615ff8a5b53ddf7e5e9e46c/djstripe/models/webhooks.py#L81-85
I'm sorry, I can't reproduce this issue. Could you open a new issue, with a traceback?
It's somewhat difficult to understand why using sentry with django_rest_framework does not upload request bodies out of the box. There were a few scattered SO and github threads about the problem but the few that I found refer to the older sentry client. I managed to track down the problem and wanted to share my results.

1. DRF uses its own json parser and calls request.read directly, without storing the result on django's request._body
2. Sentry attempts to read from django's request.body, which then errors because the stream has already been read and not assigned to request._body

Here are relevant parts of the code that I found:

https://github.com/encode/django-rest-framework/blob/3.9.0/rest_framework/parsers.py#L68

https://github.com/django/django/blob/2.1.5/django/http/request.py#L283

One workaround is to do something silly like this:

```
@api_view(['POST'])
def my_view(request):
    request.body # side effect of storing the whole request body in memory on request._request._body
    do_something(request.data)
```

I put up a repo demonstrating this. 

https://github.com/cwmoo740/django_sentry_debugging

It would be significantly better imo if there was a separate integration for DjangoRestFramework that simply read from request.data directly and let DRF do the parsing, as you suggested when you opened this thread.

If that work will be accepted I can tackle it and put up a pr.
    
Hi, I've also faced this issue when using DRF 3.9.0 and sentry-sdk 0.7.8 (0.7.4 also has this). With new unified sdk request bodies aren't sent to Sentry UI. 

I was able to track it down to the place where issue occurs.

When extracting data from request, sdk tries to get body [here](https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/integrations/_wsgi_common.py#L97), but it's impossible due to the `You cannot access body after reading from request's data stream` error. It is raised when sdk is trying to read `self.request.body` inside the called function in [django integration](https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/integrations/django/__init__.py#L255).
@cwmoo740 I'm very sorry I dropped the ball on this. Your investigation is very complete and I will fix this in the next release.

I think the primary confusion for me was that `request.body` does not get populated when `request.data` is accessed. I assumed that implementing access to `request.data` was not essential for being able to capture the request body of DRF apps.

Resolving patch:
diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py
--- a/sentry_sdk/integrations/django/__init__.py
+++ b/sentry_sdk/integrations/django/__init__.py
@@ -265,6 +265,12 @@ def files(self):
     def size_of_file(self, file):
         return file.size
 
+    def parsed_body(self):
+        try:
+            return self.request.data
+        except AttributeError:
+            return RequestExtractor.parsed_body(self)
+
 
 def _set_user_info(request, event):
     # type: (WSGIRequest, Dict[str, Any]) -> None


Test patch:
diff --git a/tests/integrations/django/myapp/urls.py b/tests/integrations/django/myapp/urls.py
--- a/tests/integrations/django/myapp/urls.py
+++ b/tests/integrations/django/myapp/urls.py
@@ -32,5 +32,13 @@
     path("template-exc", views.template_exc, name="template_exc"),
 ]
 
+
+try:
+    urlpatterns.append(
+        path("rest-framework-exc", views.rest_framework_exc, name="rest_framework_exc")
+    )
+except AttributeError:
+    pass
+
 handler500 = views.handler500
 handler404 = views.handler404
diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py
--- a/tests/integrations/django/myapp/views.py
+++ b/tests/integrations/django/myapp/views.py
@@ -4,6 +4,18 @@
 from django.shortcuts import render
 from django.views.generic import ListView
 
+try:
+    from rest_framework.decorators import api_view
+
+    @api_view(["POST"])
+    def rest_framework_exc(request):
+        1 / 0
+
+
+except ImportError:
+    pass
+
+
 import sentry_sdk
 
 
diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py
--- a/tests/integrations/django/test_basic.py
+++ b/tests/integrations/django/test_basic.py
@@ -1,4 +1,5 @@
 import pytest
+import json
 
 from werkzeug.test import Client
 from django.contrib.auth.models import User
@@ -389,3 +390,68 @@ def test_template_exception(sentry_init, client, capture_events):
         (None, None),
         (u"invalid_block_tag", u"django.template.base"),
     ]
+
+
+@pytest.mark.parametrize(
+    "type,event_request",
+    [
+        [
+            "json",
+            {
+                "cookies": {},
+                "data": {"foo": "bar"},
+                "env": {"SERVER_NAME": "localhost", "SERVER_PORT": "80"},
+                "headers": {
+                    "Content-Length": "14",
+                    "Content-Type": "application/json",
+                    "Host": "localhost",
+                },
+                "method": "POST",
+                "query_string": "",
+                "url": "http://localhost/rest-framework-exc",
+            },
+        ],
+        [
+            "formdata",
+            {
+                "cookies": {},
+                "data": {"foo": "bar"},
+                "env": {"SERVER_NAME": "localhost", "SERVER_PORT": "80"},
+                "headers": {
+                    "Content-Length": "7",
+                    "Content-Type": "application/x-www-form-urlencoded",
+                    "Host": "localhost",
+                },
+                "method": "POST",
+                "query_string": "",
+                "url": "http://localhost/rest-framework-exc",
+            },
+        ],
+    ],
+)
+def test_rest_framework_basic(
+    sentry_init, client, capture_events, capture_exceptions, type, event_request
+):
+    pytest.importorskip("rest_framework")
+    sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
+    exceptions = capture_exceptions()
+    events = capture_events()
+
+    if type == "json":
+        client.post(
+            reverse("rest_framework_exc"),
+            data=json.dumps({"foo": "bar"}),
+            content_type="application/json",
+        )
+    elif type == "formdata":
+        client.post(reverse("rest_framework_exc"), data={"foo": "bar"})
+    else:
+        assert False
+
+    error, = exceptions
+    assert isinstance(error, ZeroDivisionError)
+
+    event, = events
+    assert event["exception"]["values"][0]["mechanism"]["type"] == "django"
+
+    assert event["request"] == event_request