fix(sync): 限流检测漏掉了它真实的样子
上一版加了 429 识别,但点同步按钮报的还是 JSONDecodeError——因为检测
只看 `e.response.status_code` 和异常消息,而真实到达我们手里的异常两样
都不满足:garth 对着 429 调 .json(),抛出的 JSONDecodeError 里状态码已经
丢了,消息是无用的 "Expecting value: line 1 column 1"。
响应体「Rate limited」唯一幸存的地方是异常的 `doc` 属性,改为从那里取。
同时改正上一版的测试:它用 ValueError("Rate limited") 构造,那个形态现实中
根本不会出现,所以测试通过、检测却漏掉了每一个真实的限流。现在用真的
json.JSONDecodeError 构造,并先断言「光看消息不够」。
实测同步现在返回:
RateLimited: Garmin 暂时限制了请求频率…令牌本身没有失效
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -111,11 +111,20 @@ def _note_rate_limit(user_id):
|
||||
|
||||
|
||||
def _is_rate_limited(e):
|
||||
"""429 from Garmin, however it happens to be dressed."""
|
||||
"""429 from Garmin, however it happens to be dressed.
|
||||
|
||||
Usually it arrives as a JSONDecodeError, because garth calls .json() on a
|
||||
429 whose body is the plain text "Rate limited" — the status code is gone
|
||||
by the time the exception reaches us, and the message is the useless
|
||||
"Expecting value: line 1 column 1". What survives is the body itself, on
|
||||
the exception's `doc` attribute, so that is where to look.
|
||||
"""
|
||||
response = getattr(e, "response", None)
|
||||
if response is not None and getattr(response, "status_code", None) == 429:
|
||||
return True
|
||||
return "rate limit" in str(e).lower()
|
||||
|
||||
haystacks = [str(e), str(getattr(e, "doc", "") or "")]
|
||||
return any("rate limit" in h.lower() for h in haystacks)
|
||||
|
||||
|
||||
class MFARequired(RuntimeError):
|
||||
|
||||
@@ -619,8 +619,16 @@ class TestRateLimiting:
|
||||
def teardown_method(self):
|
||||
garmin_svc._rate_limited_until.clear()
|
||||
|
||||
def test_plain_text_body_is_recognised(self):
|
||||
assert garmin_svc._is_rate_limited(ValueError("Rate limited"))
|
||||
def test_the_real_shape_is_recognised(self):
|
||||
"""garth calls .json() on the 429, so what actually reaches us is a
|
||||
JSONDecodeError whose message says nothing and whose `doc` holds the
|
||||
body. An earlier version of this test asserted on a hand-made
|
||||
ValueError("Rate limited") — a shape that never occurs — and passed
|
||||
while the detector missed every real one."""
|
||||
import json
|
||||
err = json.JSONDecodeError("Expecting value", "Rate limited", 0)
|
||||
assert "rate limit" not in str(err).lower(), "the message alone is not enough"
|
||||
assert garmin_svc._is_rate_limited(err)
|
||||
|
||||
def test_status_code_is_recognised(self):
|
||||
class Resp:
|
||||
|
||||
Reference in New Issue
Block a user