Skip to content

Commit c7ccb05

Browse files
authored
Bugfixes (#343)
* set referrer same-origin required for ajax, and will be stricter than _not_ setting it security wise * handle "new-cluster" syndrome a brand new cluster will have no resident pods, can't reap what's not there * supress StopIteration errors from going to Sentry This is the intentional design, sentry-sdk is just noting it as it flies up to gunicorn ref: miguelgrinberg/flask-sock#64 ref: getsentry/sentry-python#3616 * make: add 'check', a combo of lint and type-check ruff and ty are so fast, i don't see why you'd bother invoking them separately
1 parent 7b16d97 commit c7ccb05

5 files changed

Lines changed: 94 additions & 0 deletions

File tree

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,5 @@ security-check: .state/docker-build-base
7070

7171
type-check: .state/docker-build-base
7272
docker compose run --rm base ty check cabotage
73+
74+
check: lint type-check

cabotage/celery/tasks/maintain.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ def reap_pods():
144144
pods = core_api_instance.list_pod_for_all_namespaces(
145145
label_selector="resident-pod.cabotage.io=true",
146146
)
147+
if not pods.items:
148+
return
147149
candidate = sorted(pods.items, key=lambda pod: pod.status.start_time)[0]
148150
lookback = datetime.datetime.now().replace(
149151
tzinfo=datetime.timezone.utc

cabotage/client/templates/_base.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<meta charset="utf-8" />
66
<meta name="viewport" content="width=device-width, initial-scale=1" />
77
<meta name="theme-color" content="#0f0f17" />
8+
<meta name="referrer" content="same-origin" />
89
<script>
910
// Apply theme + accent before render to prevent flash
1011
(function() {

cabotage/server/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,28 @@ class Model(Base):
9191
sock = Sock()
9292
csrf = CSRFProtect()
9393
babel = Babel()
94+
95+
96+
def _sentry_before_send(event, hint):
97+
"""Filter out the StopIteration raised by flask-sock to signal
98+
gunicorn that a WebSocket connection has closed (not an error)."""
99+
exc_info = hint.get("exc_info")
100+
if exc_info:
101+
exc_type, exc_value, tb = exc_info
102+
if exc_type is StopIteration and tb is not None:
103+
# Walk to the innermost frame
104+
while tb.tb_next:
105+
tb = tb.tb_next
106+
if "simple_websocket" in (tb.tb_frame.f_code.co_filename or ""):
107+
return None
108+
return event
109+
110+
94111
sentry_sdk.init(
95112
dsn=os.getenv("SENTRY_DSN"),
96113
integrations=[FlaskIntegration()],
97114
release=os.getenv("SOURCE_COMMIT"),
115+
before_send=_sentry_before_send,
98116
)
99117

100118

tests/test_sentry_filter.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import types
2+
3+
from cabotage.server import _sentry_before_send
4+
5+
6+
def _make_traceback(filename):
7+
"""Create a minimal traceback-like object with a single frame."""
8+
frame = types.SimpleNamespace(
9+
f_code=types.SimpleNamespace(co_filename=filename),
10+
)
11+
return types.SimpleNamespace(tb_next=None, tb_frame=frame)
12+
13+
14+
def _make_nested_traceback(filenames):
15+
"""Create a chained traceback from a list of filenames (outermost first)."""
16+
tb = None
17+
for filename in reversed(filenames):
18+
frame = types.SimpleNamespace(
19+
f_code=types.SimpleNamespace(co_filename=filename),
20+
)
21+
tb = types.SimpleNamespace(tb_next=tb, tb_frame=frame)
22+
return tb
23+
24+
25+
class TestSentryBeforeSend:
26+
def test_filters_stop_iteration_from_simple_websocket(self):
27+
tb = _make_traceback(
28+
"/lib/python3.13/site-packages/simple_websocket/__init__.py"
29+
)
30+
hint = {"exc_info": (StopIteration, StopIteration(), tb)}
31+
assert _sentry_before_send({"event": "data"}, hint) is None
32+
33+
def test_passes_stop_iteration_from_other_source(self):
34+
tb = _make_traceback("/app/cabotage/server/views.py")
35+
event = {"event": "data"}
36+
hint = {"exc_info": (StopIteration, StopIteration(), tb)}
37+
assert _sentry_before_send(event, hint) is event
38+
39+
def test_passes_other_exceptions(self):
40+
tb = _make_traceback(
41+
"/lib/python3.13/site-packages/simple_websocket/__init__.py"
42+
)
43+
event = {"event": "data"}
44+
hint = {"exc_info": (ValueError, ValueError("oops"), tb)}
45+
assert _sentry_before_send(event, hint) is event
46+
47+
def test_passes_events_without_exc_info(self):
48+
event = {"message": "hello"}
49+
assert _sentry_before_send(event, {}) is event
50+
51+
def test_walks_to_innermost_frame(self):
52+
tb = _make_nested_traceback(
53+
[
54+
"/app/cabotage/server/__init__.py",
55+
"/lib/python3.13/site-packages/flask/app.py",
56+
"/lib/python3.13/site-packages/simple_websocket/__init__.py",
57+
]
58+
)
59+
hint = {"exc_info": (StopIteration, StopIteration(), tb)}
60+
assert _sentry_before_send({"event": "data"}, hint) is None
61+
62+
def test_does_not_filter_when_innermost_is_not_websocket(self):
63+
tb = _make_nested_traceback(
64+
[
65+
"/lib/python3.13/site-packages/simple_websocket/__init__.py",
66+
"/app/cabotage/server/views.py",
67+
]
68+
)
69+
event = {"event": "data"}
70+
hint = {"exc_info": (StopIteration, StopIteration(), tb)}
71+
assert _sentry_before_send(event, hint) is event

0 commit comments

Comments
 (0)