Skip to content

Commit

Permalink
fix(falcon): Don't exhaust request body stream
Browse files Browse the repository at this point in the history
  • Loading branch information
szokeasaurusrex committed Nov 13, 2024
1 parent 4bec4a4 commit 80164b7
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 23 deletions.
44 changes: 23 additions & 21 deletions sentry_sdk/integrations/falcon.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
FALCON3 = False


_FALCON_UNSET = None # type: Optional[object]
if FALCON3: # falcon.request._UNSET is only available in Falcon 3.0+
with capture_internal_exceptions():
from falcon.request import _UNSET as _FALCON_UNSET # type: ignore[import-not-found, no-redef]


class FalconRequestExtractor(RequestExtractor):
def env(self):
# type: () -> Dict[str, Any]
Expand Down Expand Up @@ -73,27 +79,23 @@ def raw_data(self):
else:
return None

if FALCON3:

def json(self):
# type: () -> Optional[Dict[str, Any]]
try:
return self.request.media
except falcon.errors.HTTPBadRequest:
return None

else:

def json(self):
# type: () -> Optional[Dict[str, Any]]
try:
return self.request.media
except falcon.errors.HTTPBadRequest:
# NOTE(jmagnusson): We return `falcon.Request._media` here because
# falcon 1.4 doesn't do proper type checking in
# `falcon.Request.media`. This has been fixed in 2.0.
# Relevant code: https://github.com/falconry/falcon/blob/1.4.1/falcon/request.py#L953
return self.request._media
def json(self):
# type: () -> Optional[Dict[str, Any]]
# fallback to cached_media = None if self.request._media is not available
cached_media = None
with capture_internal_exceptions():
# self.request._media is the cached self.request.media
# value. It is only available if self.request.media
# has already been accessed. Therefore, reading
# self.request._media will not exhaust the raw request
# stream (self.request.bounded_stream) because it has
# already been read if self.request._media is set.
cached_media = self.request._media

if cached_media is not _FALCON_UNSET:
return cached_media

return None


class SentryFalconMiddleware:
Expand Down
52 changes: 50 additions & 2 deletions tests/integrations/falcon/test_falcon.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,12 @@ def test_falcon_large_json_request(sentry_init, capture_events):

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

assert_passed = False

class Resource:
def on_post(self, req, resp):
assert req.media == data
nonlocal assert_passed
assert_passed = req.media == data
sentry_sdk.capture_message("hi")
resp.media = "ok"

Expand All @@ -225,7 +228,7 @@ def on_post(self, req, resp):
client = falcon.testing.TestClient(app)
response = client.simulate_post("/", json=data)
assert response.status == falcon.HTTP_200

assert assert_passed
(event,) = events
assert event["_meta"]["request"]["data"]["foo"]["bar"] == {
"": {"len": 2000, "rem": [["!limit", "x", 1021, 1024]]}
Expand Down Expand Up @@ -460,3 +463,48 @@ def test_span_origin(sentry_init, capture_events, make_client):
(_, event) = events

assert event["contexts"]["trace"]["origin"] == "auto.http.falcon"


def test_falcon_request_media(sentry_init):
# test_passed stores whether the test has passed.
test_passed = False

# test_failure_reason stores the reason why the test failed
# if test_passed is False. The value is meaningless when
# test_passed is True.
test_failure_reason = "test endpoint did not get called"

class SentryCaptureMiddleware:
def process_request(self, _req, _resp):
# This capture message forces Falcon event processors to run
# before the request handler runs
sentry_sdk.capture_message("Processing request")

class RequestMediaResource:
def on_post(self, req, _):
nonlocal test_passed, test_failure_reason
raw_data = req.bounded_stream.read()

# If the raw_data is empty, the request body stream
# has been exhausted by the SDK. Test should fail in
# this case.
test_passed = raw_data != b""
test_failure_reason = "request body has been read"

sentry_init(integrations=[FalconIntegration()])

try:
app_class = falcon.App # Falcon ≥3.0
except AttributeError:
app_class = falcon.API # Falcon <3.0

app = app_class(middleware=[SentryCaptureMiddleware()])
app.add_route("/read_body", RequestMediaResource())

client = falcon.testing.TestClient(app)

client.simulate_post("/read_body", json={"foo": "bar"})

# Check that simulate_post actually calls the resource, and
# that the SDK does not exhaust the request body stream.
assert test_passed, test_failure_reason

0 comments on commit 80164b7

Please sign in to comment.