Skip to content

Commit 91676ec

Browse files
authored
Handling asgi body in the right way. For real (#2513)
Handling the request body in ASGI applications. By reading the body first it gets cached (by for example Django) which makes it possible to read the body multiple times.
1 parent 5c17491 commit 91676ec

File tree

6 files changed

+143
-24
lines changed

6 files changed

+143
-24
lines changed

sentry_sdk/integrations/_wsgi_common.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import absolute_import
2+
13
import json
24
from copy import deepcopy
35

@@ -7,6 +9,12 @@
79

810
from sentry_sdk._types import TYPE_CHECKING
911

12+
try:
13+
from django.http.request import RawPostDataException
14+
except ImportError:
15+
RawPostDataException = None
16+
17+
1018
if TYPE_CHECKING:
1119
import sentry_sdk
1220

@@ -67,10 +75,22 @@ def extract_into_event(self, event):
6775
if not request_body_within_bounds(client, content_length):
6876
data = AnnotatedValue.removed_because_over_size_limit()
6977
else:
78+
# First read the raw body data
79+
# It is important to read this first because if it is Django
80+
# it will cache the body and then we can read the cached version
81+
# again in parsed_body() (or json() or wherever).
82+
raw_data = None
83+
try:
84+
raw_data = self.raw_data()
85+
except (RawPostDataException, ValueError):
86+
# If DjangoRestFramework is used it already read the body for us
87+
# so reading it here will fail. We can ignore this.
88+
pass
89+
7090
parsed_body = self.parsed_body()
7191
if parsed_body is not None:
7292
data = parsed_body
73-
elif self.raw_data():
93+
elif raw_data:
7494
data = AnnotatedValue.removed_because_raw_data()
7595
else:
7696
data = None

sentry_sdk/integrations/django/asgi.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,6 @@ def sentry_patched_create_request(self, *args, **kwargs):
9494

9595
with hub.configure_scope() as scope:
9696
request, error_response = old_create_request(self, *args, **kwargs)
97-
98-
# read the body once, to signal Django to cache the body stream
99-
# so we can read the body in our event processor
100-
# (otherwise Django closes the body stream and makes it impossible to read it again)
101-
_ = request.body
102-
10397
scope.add_event_processor(_make_asgi_request_event_processor(request))
10498

10599
return request, error_response

sentry_sdk/integrations/django/views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ def sentry_patched_make_view_atomic(self, *args, **kwargs):
4747

4848
hub = Hub.current
4949
integration = hub.get_integration(DjangoIntegration)
50-
5150
if integration is not None and integration.middleware_spans:
52-
if (
51+
is_async_view = (
5352
iscoroutinefunction is not None
5453
and wrap_async_view is not None
5554
and iscoroutinefunction(callback)
56-
):
55+
)
56+
if is_async_view:
5757
sentry_wrapped_callback = wrap_async_view(hub, callback)
5858
else:
5959
sentry_wrapped_callback = _wrap_sync_view(hub, callback)
308 Bytes
Loading

tests/integrations/django/asgi/test_asgi.py

Lines changed: 116 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import base64
12
import json
3+
import os
24

35
import django
46
import pytest
@@ -370,16 +372,105 @@ async def test_trace_from_headers_if_performance_disabled(sentry_init, capture_e
370372
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
371373

372374

375+
PICTURE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "image.png")
376+
BODY_FORM = """--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="username"\r\n\r\nJane\r\n--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="password"\r\n\r\nhello123\r\n--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="photo"; filename="image.png"\r\nContent-Type: image/png\r\nContent-Transfer-Encoding: base64\r\n\r\n{{image_data}}\r\n--fd721ef49ea403a6--\r\n""".replace(
377+
"{{image_data}}", base64.b64encode(open(PICTURE, "rb").read()).decode("utf-8")
378+
).encode(
379+
"utf-8"
380+
)
381+
BODY_FORM_CONTENT_LENGTH = str(len(BODY_FORM)).encode("utf-8")
382+
383+
373384
@pytest.mark.parametrize("application", APPS)
374385
@pytest.mark.parametrize(
375-
"body,expected_return_data",
386+
"send_default_pii,method,headers,url_name,body,expected_data",
376387
[
377388
(
389+
True,
390+
"POST",
391+
[(b"content-type", b"text/plain")],
392+
"post_echo_async",
393+
b"",
394+
None,
395+
),
396+
(
397+
True,
398+
"POST",
399+
[(b"content-type", b"text/plain")],
400+
"post_echo_async",
401+
b"some raw text body",
402+
"",
403+
),
404+
(
405+
True,
406+
"POST",
407+
[(b"content-type", b"application/json")],
408+
"post_echo_async",
378409
b'{"username":"xyz","password":"xyz"}',
379410
{"username": "xyz", "password": "xyz"},
380411
),
381-
(b"hello", ""),
382-
(b"", None),
412+
(
413+
True,
414+
"POST",
415+
[(b"content-type", b"application/xml")],
416+
"post_echo_async",
417+
b'<?xml version="1.0" encoding="UTF-8"?><root></root>',
418+
"",
419+
),
420+
(
421+
True,
422+
"POST",
423+
[
424+
(b"content-type", b"multipart/form-data; boundary=fd721ef49ea403a6"),
425+
(b"content-length", BODY_FORM_CONTENT_LENGTH),
426+
],
427+
"post_echo_async",
428+
BODY_FORM,
429+
{"password": "hello123", "photo": "", "username": "Jane"},
430+
),
431+
(
432+
False,
433+
"POST",
434+
[(b"content-type", b"text/plain")],
435+
"post_echo_async",
436+
b"",
437+
None,
438+
),
439+
(
440+
False,
441+
"POST",
442+
[(b"content-type", b"text/plain")],
443+
"post_echo_async",
444+
b"some raw text body",
445+
"",
446+
),
447+
(
448+
False,
449+
"POST",
450+
[(b"content-type", b"application/json")],
451+
"post_echo_async",
452+
b'{"username":"xyz","password":"xyz"}',
453+
{"username": "xyz", "password": "[Filtered]"},
454+
),
455+
(
456+
False,
457+
"POST",
458+
[(b"content-type", b"application/xml")],
459+
"post_echo_async",
460+
b'<?xml version="1.0" encoding="UTF-8"?><root></root>',
461+
"",
462+
),
463+
(
464+
False,
465+
"POST",
466+
[
467+
(b"content-type", b"multipart/form-data; boundary=fd721ef49ea403a6"),
468+
(b"content-length", BODY_FORM_CONTENT_LENGTH),
469+
],
470+
"post_echo_async",
471+
BODY_FORM,
472+
{"password": "[Filtered]", "photo": "", "username": "Jane"},
473+
),
383474
],
384475
)
385476
@pytest.mark.asyncio
@@ -388,28 +479,42 @@ async def test_trace_from_headers_if_performance_disabled(sentry_init, capture_e
388479
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
389480
)
390481
async def test_asgi_request_body(
391-
sentry_init, capture_envelopes, application, body, expected_return_data
482+
sentry_init,
483+
capture_envelopes,
484+
application,
485+
send_default_pii,
486+
method,
487+
headers,
488+
url_name,
489+
body,
490+
expected_data,
392491
):
393-
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
492+
sentry_init(
493+
send_default_pii=send_default_pii,
494+
integrations=[
495+
DjangoIntegration(),
496+
],
497+
)
394498

395499
envelopes = capture_envelopes()
396500

397501
comm = HttpCommunicator(
398502
application,
399-
method="POST",
400-
path=reverse("post_echo_async"),
503+
method=method,
504+
headers=headers,
505+
path=reverse(url_name),
401506
body=body,
402-
headers=[(b"content-type", b"application/json")],
403507
)
404508
response = await comm.get_response()
405-
406509
assert response["status"] == 200
510+
511+
await comm.wait()
407512
assert response["body"] == body
408513

409514
(envelope,) = envelopes
410515
event = envelope.get_event()
411516

412-
if expected_return_data is not None:
413-
assert event["request"]["data"] == expected_return_data
517+
if expected_data is not None:
518+
assert event["request"]["data"] == expected_data
414519
else:
415520
assert "data" not in event["request"]

tests/integrations/django/myapp/views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,10 @@ def thread_ids_sync(*args, **kwargs):
237237
)
238238

239239
exec(
240-
"""@csrf_exempt
241-
def post_echo_async(request):
240+
"""async def post_echo_async(request):
242241
sentry_sdk.capture_message("hi")
243-
return HttpResponse(request.body)"""
242+
return HttpResponse(request.body)
243+
post_echo_async.csrf_exempt = True"""
244244
)
245245
else:
246246
async_message = None

0 commit comments

Comments
 (0)