Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Django integration: You cannot access body after reading from request's data stream #3045

Closed
Audiopolis opened this issue May 4, 2024 · 5 comments · Fixed by #3553
Closed
Labels
Type: Bug Something isn't working

Comments

@Audiopolis
Copy link

How do you use Sentry?

Sentry Saas (sentry.io)

Version

2.0.1

Steps to Reproduce

Environment info:
django=5.0.4
djangorestframework=3.15.1

  1. Add sentry integration to django app:
if ENV != "development":
    sentry_sdk.init(
        dsn="https://....ingest.sentry.io/6010199",
        integrations=[
            DjangoIntegration(
                transaction_style="url",
                middleware_spans=True,
                signals_spans=True,
                cache_spans=False,
            )
        ],
        debug=DEBUG,  # This is true in this case
        send_default_pii=True,
        environment=ENV,
    )
  1. `logger.exception("This is an exception")
  2. See traceback

Expected Result

Either the exception is successfully logged to Sentry, or it's not sent to Sentry at all since it's a log entry and not an unhandled exception.

Actual Result

# Traceback for the `logger.exception` call goes here

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/sentry_sdk/integrations/django/__init__.py", line 555, in parsed_body
    return self.request.data
           ^^^^^^^^^^^^^^^^^
AttributeError: 'ASGIRequest' object has no attribute 'data'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/sentry_sdk/integrations/django/asgi.py", line 56, in asgi_request_event_processor
    DjangoRequestExtractor(request).extract_into_event(event)
  File "/usr/local/lib/python3.12/site-packages/sentry_sdk/integrations/_wsgi_common.py", line 96, in extract_into_event
    parsed_body = self.parsed_body()
                  ^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/sentry_sdk/integrations/django/__init__.py", line 557, in parsed_body
    return RequestExtractor.parsed_body(self)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/sentry_sdk/integrations/_wsgi_common.py", line 142, in parsed_body
    return self.json()
           ^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/sentry_sdk/integrations/_wsgi_common.py", line 154, in json
    raw_data = self.raw_data()
               ^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/sentry_sdk/integrations/django/__init__.py", line 538, in raw_data
    return self.request.body
           ^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/django/http/request.py", line 328, in body
    raise RawPostDataException(
django.http.request.RawPostDataException: You cannot access body after reading from request's data stream
szokeasaurusrex added a commit to szokeasaurusrex/issue-reproductions that referenced this issue May 6, 2024
@szokeasaurusrex
Copy link
Member

Hi @Audiopolis, I attempted to reproduce your issue using this code, but I did not observe the error message that you reported.

Can you please clarify how exactly you encountered this issue? If possible, please share a reproduction with me (you can also do so by opening a PR on my reproduction repo and sharing a link to the PR here).

@Audiopolis
Copy link
Author

@szokeasaurusrex Thanks for trying. I will try to create a reproduction. In the meantime, I'll clarify how I encountered the issue:

We send an email from a Django view using Resend. In this environment, Resend does not recognize the domain, so it throws an error. We catch that error and use logger.exception(...) instead, where logger = logging.getLogger(__name__). The view returns a 201 response. Then, the exact traceback I added to this issue description is printed.

The original request is automatically converted from an ASGIRequest (no data attribute, and can only read body once) to a RES framework Request (has a cached data property) by REST framework. I don't know how Sentry gets the original ASGIRequest instead of the DRF Request, but since it does, it makes sense that 'ASGIRequest' object has no attribute 'data' and You cannot access body after reading from request's data stream. I will have to analyze the situation more closely and provide a reproduction.

@antonpirker
Copy link
Member

Same error is also happening inside Sentry with SDK 2.1.1:

  File "/usr/local/lib/python3.11/site-packages/sentry_sdk/integrations/django/__init__.py", line 555, in parsed_body
    return self.request.data
           ^^^^^^^^^^^^^^^^^
AttributeError: 'WSGIRequest' object has no attribute 'data'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/sentry_sdk/integrations/django/__init__.py", line 486, in wsgi_request_event_processor
    DjangoRequestExtractor(request).extract_into_event(event)
  File "/usr/local/lib/python3.11/site-packages/sentry_sdk/integrations/_wsgi_common.py", line 96, in extract_into_event
    parsed_body = self.parsed_body()
                  ^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sentry_sdk/integrations/django/__init__.py", line 557, in parsed_body
    return RequestExtractor.parsed_body(self)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sentry_sdk/integrations/_wsgi_common.py", line 142, in parsed_body
    return self.json()
           ^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sentry_sdk/integrations/_wsgi_common.py", line 154, in json
    raw_data = self.raw_data()
               ^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sentry_sdk/integrations/django/__init__.py", line 538, in raw_data
    return self.request.body
           ^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/http/request.py", line 328, in body
    raise RawPostDataException(
django.http.request.RawPostDataException: You cannot access body after reading from request's data stream", "level":"error", "name":"sentry_sdk.errors"}

Can be seen here: https://cloudlogging.app.goo.gl/yfAdqBdmA8iER8ZQ8 (retricted link)

@sentrivana sentrivana self-assigned this May 13, 2024
@sentrivana sentrivana added Type: Bug Something isn't working and removed Waiting for: Community labels May 13, 2024
@sentrivana
Copy link
Contributor

So just to expand on what was already written here, the way DRF works is that it wraps the native Django WSGIRequest (or ASGIRequest) in its own Request. The wrapper class adds some stuff on top and proxies everything else to the underlying Django request.

We capture the data here. Basically, self.request.data is only expected to be there if this is a DRF Request; otherwise we essentially fall back to self.request.body (that's what RequestExtractor.parsed_body(self) eventually looks at). Reading the body on a Django request should be fine but in some cases the body has already started being read and we hit this.

The quick "fix" would be to capture RawPostDataException when we try to access request.body and just give up on reading the body ourselves and return None. However this doesn't address the underlying problem of why request.data is not there in the first place (it indeed looks like we're working with the Django request instead of the DRF one).

Wasn't able to repro this yet -- @Audiopolis What does the request look like in your app? I assume it's a POST? Does it contain any form data, uploaded files, etc.? Do you access anything on the request in the view?

@Audiopolis
Copy link
Author

So just to expand on what was already written here, the way DRF works is that it wraps the native Django WSGIRequest (or ASGIRequest) in its own Request. The wrapper class adds some stuff on top and proxies everything else to the underlying Django request.

We capture the data here. Basically, self.request.data is only expected to be there if this is a DRF Request; otherwise we essentially fall back to self.request.body (that's what RequestExtractor.parsed_body(self) eventually looks at). Reading the body on a Django request should be fine but in some cases the body has already started being read and we hit this.

The quick "fix" would be to capture RawPostDataException when we try to access request.body and just give up on reading the body ourselves and return None. However this doesn't address the underlying problem of why request.data is not there in the first place (it indeed looks like we're working with the Django request instead of the DRF one).

Wasn't able to repro this yet -- @Audiopolis What does the request look like in your app? I assume it's a POST? Does it contain any form data, uploaded files, etc.? Do you access anything on the request in the view?

Thanks for the additional context @sentrivana. I don't have the full body anymore, but I believe it was indeed a POST request containing a JSON payload. All the logic succeeded (including feeding the payload into a serializer that validated the data and used it to create an object) until it attempted to send an email through a third-party service, which failed, triggering an exception log. In other words, the DRF request has been read multiple times, and at some point, it seems to stop being a DRF request.

We are using this middleware:

MIDDLEWARE = [
   "django.middleware.security.SecurityMiddleware",
   "django.contrib.sessions.middleware.SessionMiddleware",
   "corsheaders.middleware.CorsMiddleware",
   "django.middleware.common.CommonMiddleware",
   "django.middleware.csrf.CsrfViewMiddleware",
   "django.contrib.auth.middleware.AuthenticationMiddleware",
   "django.contrib.messages.middleware.MessageMiddleware",
   "django.middleware.clickjacking.XFrameOptionsMiddleware",
   "nplusone.ext.django.NPlusOneMiddleware",
]

In this environment, we are also using debug_toolbar.middleware.DebugToolbarMiddleware.

I have not had time yet to try to create a reproduction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Bug Something isn't working
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

5 participants