Skip to content

Commit 87cf66e

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

File tree

2 files changed

+63
-21
lines changed

2 files changed

+63
-21
lines changed

sentry_sdk/integrations/falcon.py

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

4545

46+
_FALCON_UNSET = None # type: Optional[object]
47+
with capture_internal_exceptions():
48+
from falcon._typing import _UNSET as _FALCON_UNSET
49+
50+
4651
class FalconRequestExtractor(RequestExtractor):
4752
def env(self):
4853
# type: () -> Dict[str, Any]
@@ -73,27 +78,23 @@ def raw_data(self):
7378
else:
7479
return None
7580

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
81+
def json(self):
82+
# type: () -> Optional[Dict[str, Any]]
83+
if _FALCON_UNSET is not None:
84+
cached_media = None
85+
with capture_internal_exceptions():
86+
# self.request._media is the cached self.request.media
87+
# value. It is only available if self.request.media
88+
# has already been accessed. Therefore, reading
89+
# self.request._media will not exhaust the raw request
90+
# stream (self.request.bounded_stream) because it has
91+
# already been read if self.request._media is set.
92+
cached_media = self.request._media
93+
94+
if cached_media is not _FALCON_UNSET:
95+
return cached_media
96+
97+
return None
9798

9899

99100
class SentryFalconMiddleware:

tests/integrations/falcon/test_falcon.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,44 @@ 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, make_app):
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+
app = make_app()
494+
app.add_middleware(SentryCaptureMiddleware())
495+
app.add_route("/read_body", RequestMediaResource())
496+
497+
client = falcon.testing.TestClient(app)
498+
499+
client.simulate_post("/read_body", json={"foo": "bar"})
500+
501+
# Check that simulate_post actually calls the resource, and
502+
# that the SDK does not exhaust the request body stream.
503+
assert test_passed, test_failure_reason

0 commit comments

Comments
 (0)