Skip to content

Commit 18073f8

Browse files
committed
fix(gooddata-eval): fall back to the default score retry delay for negative or NaN Retry-After
time.sleep rejects a negative or NaN delay, so a skewed or hostile Retry-After header would turn a throttled score write into an exception instead of a retry. Such values now use the default delay; the existing cap still bounds long waits. jira: trivial risk: low
1 parent bbe1187 commit 18073f8

2 files changed

Lines changed: 27 additions & 2 deletions

File tree

‎packages/gooddata-eval/src/gooddata_eval/core/langfuse/client.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,18 @@ def _is_retryable(resp: httpx.Response) -> bool:
3232

3333

3434
def _retry_delay(resp: httpx.Response) -> float:
35-
"""Seconds to wait before the next attempt, from `Retry-After` when the server names one."""
35+
"""Seconds to wait before the next attempt, from `Retry-After` when the server names one.
36+
37+
Unparsable, negative or NaN values fall back to the default; the cap bounds the wait so a
38+
throttled score cannot hold a linking worker for long.
39+
"""
3640
try:
37-
return min(float(resp.headers.get("Retry-After", "")), _MAX_RETRY_DELAY)
41+
asked_for = float(resp.headers.get("Retry-After", ""))
3842
except ValueError:
3943
return _DEFAULT_RETRY_DELAY
44+
if not asked_for >= 0:
45+
return _DEFAULT_RETRY_DELAY
46+
return min(asked_for, _MAX_RETRY_DELAY)
4047

4148

4249
class _TraceListResult:

‎packages/gooddata-eval/tests/test_langfuse_client.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,24 @@ def test_a_retry_delay_the_server_did_not_name_falls_back_to_a_short_one(make_cl
139139
assert slept == [0.5]
140140

141141

142+
@pytest.mark.parametrize("header", ["-5", "nan"])
143+
def test_a_retry_after_that_cannot_be_slept_falls_back_to_the_default(header):
144+
# time.sleep rejects a negative or NaN delay, so either would turn a retry into an exception.
145+
assert client_module._retry_delay(httpx.Response(429, headers={"Retry-After": header})) == 0.5
146+
147+
148+
def test_a_retry_after_beyond_the_cap_is_clamped():
149+
assert client_module._retry_delay(httpx.Response(429, headers={"Retry-After": "60"})) == 5.0
150+
151+
152+
def test_a_retry_after_given_as_a_date_falls_back_to_the_default():
153+
# Retry-After is allowed to be an HTTP-date; this client reads seconds only. Langfuse
154+
# documents the header as a number of seconds, and the cap already bounds the wait, so
155+
# parsing a date could only turn the 0.5s fallback into the same 5s ceiling.
156+
response = httpx.Response(429, headers={"Retry-After": "Wed, 21 Oct 2026 07:28:00 GMT"})
157+
assert client_module._retry_delay(response) == 0.5
158+
159+
142160
def test_a_score_gives_up_after_three_attempts(make_client, monkeypatch):
143161
monkeypatch.setattr(client_module.time, "sleep", lambda _seconds: None)
144162
attempts = 0

0 commit comments

Comments
 (0)