From 7bac80beb78653b48b22b545300081b69a9819f5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 15 Sep 2026 13:33:23 +0200 Subject: [PATCH 1/6] fix(bottle): Avoid infinite serializing loop --- sentry_sdk/integrations/bottle.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sentry_sdk/integrations/bottle.py b/sentry_sdk/integrations/bottle.py index 8f3ede8190..ffbd6754f8 100644 --- a/sentry_sdk/integrations/bottle.py +++ b/sentry_sdk/integrations/bottle.py @@ -75,6 +75,23 @@ def setup_once() -> None: version = parse_version(BOTTLE_VERSION) _check_minimum_version(BottleIntegration, version) + # Bottle's Route.__repr__ calls get_undecorated_callback() which has + # a while-True loop that hangs when the callback is a closure whose + # cells don't contain any callable. This is triggered when + # attach_stacktrace=True serializes local variables including Route + # objects via safe_repr → repr(). Providing __sentry_repr__ makes the + # serializer use it instead of repr(). + def _sentry_route_repr(self: "Route") -> str: + cb = self.callback + return "<%s %s -> %s:%s>" % ( + self.method, + self.rule, + getattr(cb, "__module__", "?"), + getattr(cb, "__name__", "?"), + ) + + Route.__sentry_repr__ = _sentry_route_repr + old_app = Bottle.__call__ @ensure_integration_enabled(BottleIntegration, old_app) From 45337c4bfa87bcf5342f8dd162554fd3d361f3a5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 15 Sep 2026 13:38:35 +0200 Subject: [PATCH 2/6] test --- tests/integrations/bottle/test_bottle.py | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/integrations/bottle/test_bottle.py b/tests/integrations/bottle/test_bottle.py index b224d59753..3ad138d188 100644 --- a/tests/integrations/bottle/test_bottle.py +++ b/tests/integrations/bottle/test_bottle.py @@ -166,6 +166,37 @@ def index(): assert len(event["request"]["data"]["foo"]["bar"]) == 1034 +def test_attach_stacktrace_works(sentry_init, capture_events, app, get_client): + sentry_init( + integrations=[BottleIntegration()], + max_request_body_size="always", + attach_stacktrace=True, + ) + + data = {"foo": {"bar": "a" * (1024)}} + + @app.route("/", method="POST") + def index(): + import bottle + + assert bottle.request.json == data + assert bottle.request.body.read() == json.dumps(data).encode("ascii") + capture_message("hi") + return "ok" + + events = capture_events() + + client = get_client() + response = client.get("/") + + response = client.post("/", content_type="application/json", data=json.dumps(data)) + assert response[1] == "200 OK" + + (event,) = events + + assert len(event["request"]["data"]["foo"]["bar"]) == 1024 + + @pytest.mark.parametrize("data", [{}, []], ids=["empty-dict", "empty-list"]) def test_empty_json_request(sentry_init, capture_events, app, data, get_client): sentry_init(integrations=[BottleIntegration()]) From b56ee551cfe616e6800f0aa9b54dab6130ea1c0a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 15 Sep 2026 13:39:54 +0200 Subject: [PATCH 3/6] humanize --- sentry_sdk/integrations/bottle.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/bottle.py b/sentry_sdk/integrations/bottle.py index ffbd6754f8..0b5e885215 100644 --- a/sentry_sdk/integrations/bottle.py +++ b/sentry_sdk/integrations/bottle.py @@ -75,12 +75,9 @@ def setup_once() -> None: version = parse_version(BOTTLE_VERSION) _check_minimum_version(BottleIntegration, version) - # Bottle's Route.__repr__ calls get_undecorated_callback() which has - # a while-True loop that hangs when the callback is a closure whose - # cells don't contain any callable. This is triggered when - # attach_stacktrace=True serializes local variables including Route - # objects via safe_repr → repr(). Providing __sentry_repr__ makes the - # serializer use it instead of repr(). + # Bottle's Route.__repr__ might lead to a never-terminating while True + # loop. This is triggered when attach_stacktrace=True serializes local + # variables. def _sentry_route_repr(self: "Route") -> str: cb = self.callback return "<%s %s -> %s:%s>" % ( From 1f3a80dabe4bee42fdd98dc7546a55b7c71259cd Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 15 Sep 2026 13:44:33 +0200 Subject: [PATCH 4/6] . --- sentry_sdk/integrations/bottle.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/bottle.py b/sentry_sdk/integrations/bottle.py index 0b5e885215..68df00cfde 100644 --- a/sentry_sdk/integrations/bottle.py +++ b/sentry_sdk/integrations/bottle.py @@ -76,8 +76,7 @@ def setup_once() -> None: _check_minimum_version(BottleIntegration, version) # Bottle's Route.__repr__ might lead to a never-terminating while True - # loop. This is triggered when attach_stacktrace=True serializes local - # variables. + # loop when attach_stacktrace=True. def _sentry_route_repr(self: "Route") -> str: cb = self.callback return "<%s %s -> %s:%s>" % ( From 6d9ca3699fecd688c213a1fabf474db83695a1d0 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 15 Sep 2026 13:53:47 +0200 Subject: [PATCH 5/6] comment --- tests/integrations/bottle/test_bottle.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integrations/bottle/test_bottle.py b/tests/integrations/bottle/test_bottle.py index 3ad138d188..781f0b3e9a 100644 --- a/tests/integrations/bottle/test_bottle.py +++ b/tests/integrations/bottle/test_bottle.py @@ -194,7 +194,9 @@ def index(): (event,) = events - assert len(event["request"]["data"]["foo"]["bar"]) == 1024 + # As long as this test finishes, we're good. It's just making sure we don't + # hang. + assert event["request"]["data"] @pytest.mark.parametrize("data", [{}, []], ids=["empty-dict", "empty-list"]) From dd336b89e84fa5e224d7e7cc095c2eb091ae6d64 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 15 Sep 2026 13:54:13 +0200 Subject: [PATCH 6/6] name --- tests/integrations/bottle/test_bottle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/bottle/test_bottle.py b/tests/integrations/bottle/test_bottle.py index 781f0b3e9a..a4e23789ea 100644 --- a/tests/integrations/bottle/test_bottle.py +++ b/tests/integrations/bottle/test_bottle.py @@ -166,7 +166,7 @@ def index(): assert len(event["request"]["data"]["foo"]["bar"]) == 1034 -def test_attach_stacktrace_works(sentry_init, capture_events, app, get_client): +def test_attach_stacktrace_doesnt_hang(sentry_init, capture_events, app, get_client): sentry_init( integrations=[BottleIntegration()], max_request_body_size="always",