Skip to content

Commit 68e1839

Browse files
fix(falcon): Don't exhaust request body stream
1 parent 417be9f commit 68e1839

File tree

2 files changed

+73
-23
lines changed

2 files changed

+73
-23
lines changed

sentry_sdk/integrations/falcon.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@
4343
FALCON3 = False
4444

4545

46+
_FALCON_UNSET = None # type: Optional[object]
47+
if FALCON3: # falcon.request._UNSET is only available in Falcon 3.0+
48+
with capture_internal_exceptions():
49+
from falcon.request import _UNSET as _FALCON_UNSET # type: ignore[import-not-found, no-redef]
50+
51+
4652
class FalconRequestExtractor(RequestExtractor):
4753
def env(self):
4854
# type: () -> Dict[str, Any]
@@ -73,27 +79,23 @@ def raw_data(self):
7379
else:
7480
return None
7581

76-
if FALCON3:
77-
78-
def json(self):
79-
# type: () -> Optional[Dict[str, Any]]
80-
try:
81-
return self.request.media
82-
except falcon.errors.HTTPBadRequest:
83-
return None
84-
85-
else:
86-
87-
def json(self):
88-
# type: () -> Optional[Dict[str, Any]]
89-
try:
90-
return self.request.media
91-
except falcon.errors.HTTPBadRequest:
92-
# NOTE(jmagnusson): We return `falcon.Request._media` here because
93-
# falcon 1.4 doesn't do proper type checking in
94-
# `falcon.Request.media`. This has been fixed in 2.0.
95-
# Relevant code: https://github.com/falconry/falcon/blob/1.4.1/falcon/request.py#L953
96-
return self.request._media
82+
def json(self):
83+
# type: () -> Optional[Dict[str, Any]]
84+
# fallback to cached_media = None if self.request._media is not available
85+
cached_media = None
86+
with capture_internal_exceptions():
87+
# self.request._media is the cached self.request.media
88+
# value. It is only available if self.request.media
89+
# has already been accessed. Therefore, reading
90+
# self.request._media will not exhaust the raw request
91+
# stream (self.request.bounded_stream) because it has
92+
# already been read if self.request._media is set.
93+
cached_media = self.request._media
94+
95+
if cached_media is not _FALCON_UNSET:
96+
return cached_media
97+
98+
return None
9799

98100

99101
class SentryFalconMiddleware:

tests/integrations/falcon/test_falcon.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,12 @@ def test_falcon_large_json_request(sentry_init, capture_events):
211211

212212
data = {"foo": {"bar": "a" * 2000}}
213213

214+
assert_passed = False
215+
214216
class Resource:
215217
def on_post(self, req, resp):
216-
assert req.media == data
218+
nonlocal assert_passed
219+
assert_passed = req.media == data
217220
sentry_sdk.capture_message("hi")
218221
resp.media = "ok"
219222

@@ -225,7 +228,7 @@ def on_post(self, req, resp):
225228
client = falcon.testing.TestClient(app)
226229
response = client.simulate_post("/", json=data)
227230
assert response.status == falcon.HTTP_200
228-
231+
assert assert_passed
229232
(event,) = events
230233
assert event["_meta"]["request"]["data"]["foo"]["bar"] == {
231234
"": {"len": 2000, "rem": [["!limit", "x", 1021, 1024]]}
@@ -460,3 +463,48 @@ def test_span_origin(sentry_init, capture_events, make_client):
460463
(_, event) = events
461464

462465
assert event["contexts"]["trace"]["origin"] == "auto.http.falcon"
466+
467+
468+
def test_falcon_request_media(sentry_init):
469+
# test_passed stores whether the test has passed.
470+
test_passed = False
471+
472+
# test_failure_reason stores the reason why the test failed
473+
# if test_passed is False. The value is meaningless when
474+
# test_passed is True.
475+
test_failure_reason = "test endpoint did not get called"
476+
477+
class SentryCaptureMiddleware:
478+
def process_request(self, _req, _resp):
479+
# This capture message forces Falcon event processors to run
480+
# before the request handler runs
481+
sentry_sdk.capture_message("Processing request")
482+
483+
class RequestMediaResource:
484+
def on_post(self, req, _):
485+
nonlocal test_passed, test_failure_reason
486+
raw_data = req.bounded_stream.read()
487+
488+
# If the raw_data is empty, the request body stream
489+
# has been exhausted by the SDK. Test should fail in
490+
# this case.
491+
test_passed = raw_data != b""
492+
test_failure_reason = "request body has been read"
493+
494+
sentry_init(integrations=[FalconIntegration()])
495+
496+
try:
497+
app_class = falcon.App # Falcon ≥3.0
498+
except AttributeError:
499+
app_class = falcon.API # Falcon <3.0
500+
501+
app = app_class(middleware=[SentryCaptureMiddleware()])
502+
app.add_route("/read_body", RequestMediaResource())
503+
504+
client = falcon.testing.TestClient(app)
505+
506+
client.simulate_post("/read_body", json={"foo": "bar"})
507+
508+
# Check that simulate_post actually calls the resource, and
509+
# that the SDK does not exhaust the request body stream.
510+
assert test_passed, test_failure_reason

0 commit comments

Comments
 (0)