Skip to content

Commit aa6e8fd

Browse files
fix(falcon): Don't exhaust request body stream (#3768)
Only read the cached `request._media`, since reading `request.media` will exhaust the `request.bounded_stream` if it has not been read before. Note that this means that we will now only send the JSON request body to Sentry if the Falcon request handler reads the JSON data. Fixes #3761 Co-authored-by: Anton Pirker <[email protected]>
1 parent 3e28853 commit aa6e8fd

File tree

2 files changed

+68
-21
lines changed

2 files changed

+68
-21
lines changed

sentry_sdk/integrations/falcon.py

+23-21
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

+45
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,48 @@ def test_span_origin(sentry_init, capture_events, make_client):
460460
(_, event) = events
461461

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

0 commit comments

Comments
 (0)