From 57c236ba1607bec05de64139c07721f2460756ee Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Thu, 3 Sep 2026 07:18:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BF=9E=E6=8E=A5=E9=98=B6=E6=AE=B5?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=20429=20=E4=BB=A5=20rate=5Flimited=20?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=91=88=E7=8E=B0=EF=BC=8C=E8=80=8C=E9=9D=9E?= =?UTF-8?q?=E6=B3=9B=E5=8C=96=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sync_data 的 _connect 抛 RateLimited(真实 429,冷却已写入)原本落进 except Exception 返回 status=error,message 带 'RateLimited:' 前缀——前端 SettingsPage 只对 status=rate_limited 渲染'被限流'提示,导致放开本地拦截 后真实 429 的提示错位。 单独捕获 RateLimited:status=rate_limited + 干净 message + history 记录 rate_limited。补测试 test_a_real_429_at_connect_surfaces_as_rate_limited (597 passed) --- backend/services/garmin.py | 14 ++++++++++++++ backend/tests/test_garmin_sync.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/backend/services/garmin.py b/backend/services/garmin.py index b0b2daf..b2f0de7 100644 --- a/backend/services/garmin.py +++ b/backend/services/garmin.py @@ -1111,6 +1111,20 @@ def sync_data(user_id, creds, days=None, client=None, trigger="manual"): try: client = client or _connect(creds, user_id) + except RateLimited as e: + # A genuine 429 at connect time: _connect already wrote the fresh + # cooldown, so surface this as rate_limited (the UI draws a distinct + # stand-down state for it) rather than a generic error. + message = str(e) + _set_sync_status(user_id, "rate_limited", now, records_synced=0, + last_error=message) + return finish({ + "status": "rate_limited", + "recordsSynced": 0, + "message": message, + "mfaRequired": False, + "lastSyncTime": now, + }) except Exception as e: message = describe(e) _set_sync_status(user_id, "error", now, records_synced=0, last_error=message) diff --git a/backend/tests/test_garmin_sync.py b/backend/tests/test_garmin_sync.py index 4a6065b..ddab01e 100644 --- a/backend/tests/test_garmin_sync.py +++ b/backend/tests/test_garmin_sync.py @@ -745,6 +745,26 @@ class TestRateLimiting: garmin_svc._connect({}, user_id=user["id"]) assert calls == [], "an unexpired token needs no refresh" + def test_a_real_429_at_connect_surfaces_as_rate_limited(self, db, user, monkeypatch): + """A genuine 429 while connecting is a stand-down, not a generic + failure: sync_data must report status rate_limited so the UI draws the + distinct '被限流' state instead of a plain error.""" + def throttled(_creds, _uid=None): + garmin_svc._note_rate_limit(user["id"]) # what _connect does on 429 + raise garmin_svc.RateLimited( + "Garmin 暂时限制了请求频率。这通常是短时间内连接过于频繁," + "等待约半小时后会自动恢复,令牌本身没有失效。" + ) + + monkeypatch.setattr(garmin_svc, "_connect", throttled) + out = garmin_svc.sync_data(user["id"], CREDS, days=1) + + assert out["status"] == "rate_limited" + assert "令牌本身没有失效" in out["message"] + assert garmin_svc.get_sync_status(user["id"])["status"] == "rate_limited" + assert garmin_svc.get_sync_history(user["id"])[0]["status"] == "rate_limited" + garmin_svc._rate_limited_until.clear() + def test_a_valid_token_is_not_refreshed(self, db, user, monkeypatch): """Refreshing on every connect spends quota for nothing — and that is what walked the account into a 429 in the first place."""