Skip to content

Commit fb377ad

Browse files
authored
Guard against unexpected status code (#5)
1 parent c4cb309 commit fb377ad

2 files changed

Lines changed: 53 additions & 13 deletions

File tree

tests/test_transmitter.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,44 @@ def test_send_301_redirect_returns_error_no_retry(self):
182182
resp = t.send({"batch_id": "b-1", "events": []})
183183

184184
assert resp.status == "error"
185+
186+
187+
@pytest.mark.parametrize("status_code", [200, 201, 203, 206])
188+
def test_send_unexpected_status_raises_transmit_error(status_code):
189+
# Regression: any status not explicitly handled must raise TransmitError rather
190+
# than falling through the elif chain and returning None. A None return would
191+
# cause AttributeError in the consumer (response.status), silently killing the
192+
# background thread and freezing the queue.
193+
t = Transmitter(api_key="test-key", host="https://app.wildedge.dev")
194+
with patch.object(
195+
t._opener, "open", return_value=_make_response(status_code, {"status": "ok"})
196+
):
197+
with pytest.raises(TransmitError, match=f"Unexpected HTTP {status_code}"):
198+
t.send({"batch_id": "b-1", "events": []})
199+
200+
201+
def test_unexpected_status_keeps_events_in_consumer_queue():
202+
# End-to-end regression: TransmitError from an unexpected status must cause the
203+
# consumer to retain events for retry, not lose them.
204+
from wildedge.consumer import Consumer
205+
from wildedge.device import DeviceInfo
206+
from wildedge.queue import EventQueue
207+
208+
queue = EventQueue(max_size=100)
209+
queue.add({"event_id": "e1", "event_type": "inference", "model_id": "m"})
210+
211+
mock_transmitter = MagicMock(spec=Transmitter)
212+
mock_transmitter.send.side_effect = TransmitError("Unexpected HTTP 200")
213+
214+
consumer = Consumer(
215+
queue=queue,
216+
transmitter=mock_transmitter,
217+
device=DeviceInfo(device_id="d", device_type="linux"),
218+
get_models=lambda: {},
219+
session_id="sess-1",
220+
)
221+
result = consumer.drain_once()
222+
consumer.stop()
223+
224+
assert result is False
225+
assert queue.length() == 1

wildedge/transmitter.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class IngestResponse:
3030

3131

3232
class TransmitError(Exception):
33-
"""Raised for retryable errors (429 / 5xx / network)."""
33+
"""Raised for retryable errors (429 / 5xx / network / unexpected status)."""
3434

3535

3636
class Transmitter:
@@ -78,8 +78,7 @@ def send(self, batch: dict) -> IngestResponse:
7878
server_time=data.get("server_time"),
7979
rejected=data.get("rejected"),
8080
)
81-
82-
if status_code == 400:
81+
elif status_code == 400:
8382
logger.warning(
8483
"wildedge: batch rejected (400) - discarding: %s",
8584
raw[: constants.ERROR_MSG_MAX_LEN],
@@ -90,17 +89,15 @@ def send(self, batch: dict) -> IngestResponse:
9089
events_accepted=0,
9190
events_rejected=len(batch.get("events", [])),
9291
)
93-
94-
if status_code == 401:
92+
elif status_code == 401:
9593
logger.error("wildedge: authentication failed (401) - check your API key")
9694
return IngestResponse(
9795
status="unauthorized",
9896
batch_id=batch.get("batch_id", ""),
9997
events_accepted=0,
10098
events_rejected=len(batch.get("events", [])),
10199
)
102-
103-
if 300 <= status_code < 400:
100+
elif 300 <= status_code < 400:
104101
# Redirects should never occur (we disable redirect following).
105102
# Treat as a permanent config error so we don't loop.
106103
logger.error(
@@ -114,8 +111,7 @@ def send(self, batch: dict) -> IngestResponse:
114111
events_accepted=0,
115112
events_rejected=len(batch.get("events", [])),
116113
)
117-
118-
if status_code == 404:
114+
elif status_code == 404:
119115
logger.error(
120116
"wildedge: endpoint not found (404) at %s; check WILDEDGE_DSN", url
121117
)
@@ -125,13 +121,11 @@ def send(self, batch: dict) -> IngestResponse:
125121
events_accepted=0,
126122
events_rejected=len(batch.get("events", [])),
127123
)
128-
129-
if status_code == 429 or status_code >= 500:
124+
elif status_code == 429 or status_code >= 500:
130125
raise TransmitError(
131126
f"HTTP {status_code}: {raw[: constants.ERROR_MSG_MAX_LEN]!r}"
132127
)
133-
134-
if 400 <= status_code < 500:
128+
elif 400 <= status_code < 500:
135129
# Other 4xx (e.g. 422 Unprocessable) are permanent client errors; discard.
136130
logger.warning(
137131
"wildedge: batch rejected (%d) - discarding: %s",
@@ -144,6 +138,11 @@ def send(self, batch: dict) -> IngestResponse:
144138
events_accepted=0,
145139
events_rejected=len(batch.get("events", [])),
146140
)
141+
else:
142+
# Guard against unexpected status codes that would otherwise return None.
143+
raise TransmitError(
144+
f"Unexpected HTTP {status_code}: {raw[: constants.ERROR_MSG_MAX_LEN]!r}"
145+
)
147146

148147
def close(self) -> None:
149148
pass

0 commit comments

Comments
 (0)