Skip to content

fix(cli): never let a Retry-After shorten a watch's pause - #114

Open
tee8z wants to merge 1 commit into
masterfrom
fix/watch-retry-after-floor
Open

tee8z wants to merge 1 commit into
masterfrom
fix/watch-retry-after-floor

Conversation

@tee8z

@tee8z tee8z commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Problem

payments list --watch (#111) waited the server's Retry-After in place of its own pause. retry_after() returns zero for Retry-After: 0, x-retry-after-ms: 0, or an HTTP date already past, which clock skew can produce. So a 429 or 5xx carrying such a hint was retried at once, again and again, for as long as the server answered that way. On a refresh that worked, the same hint could make the watch poll faster than --interval. I raised this as a nonblocking note on #111, which merged before it was addressed.

Change

watch::pause takes the longer of the hint and the watch's own pause. Retry-After is the least time the server asks for, so it can lengthen a pause but never shorten it.

Refresh Before After
Worked, no hint --interval --interval
Worked, with a hint the hint, even 0 the longer of the hint and --interval
Failed (429, 5xx, transport), no hint backoff backoff
Failed, with a hint the hint, even 0 the longer of the hint and the backoff

The backoff now advances on every failure, also one with a hint, so repeated rate limits wait longer, up to the 60-second ceiling. A refresh that works still resets it. The payments guide and the unreleased --watch changelog entry say the same.

Validation

In nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (198 unit, 150 integration) and python3 scripts/check-coverage.py passed.

  • Unit test a_retry_hint_lengthens_a_pause_but_never_shortens_it covers no hint, a zero hint, a shorter hint and a longer hint.
  • Integration test a_watch_never_refreshes_sooner_than_its_own_pause_for_a_zero_retry_hint sends Retry-After: 0 on a success and on a 429, then a 403 that ends the watch, with --interval 1. It asserts that at least 2.5 seconds passed. That is a lower bound, so it cannot flake by finishing early. With the old code the watch finished in 1.05 seconds, and the test failed.

CRAP report: 843 measured functions, none at or above 20. The highest is wait_for_payment at 18.10, which this change does not touch. The first CRAP run failed on the two pseudo-terminal login tests that #104 addresses (a_quiet_login_on_a_terminal_still_shows_the_code_but_no_closing_line and a_profile_saved_after_login_keeps_changes_made_while_its_prompt_was_open). The rerun passed.

`payments list --watch` waited the server's `Retry-After` in place of its own
pause. `retry_after()` returns zero for `Retry-After: 0`, `x-retry-after-ms: 0`,
or an HTTP date already past, which clock skew can produce, so a 429 or 5xx
carrying such a hint was retried at once, again and again, for as long as the
server answered that way. On a refresh that worked, the same hint could make the
watch poll faster than `--interval`.

`watch::pause` takes the longer of the hint and the watch's own pause:
`--interval` after a refresh that worked, the backoff after a failure.
Retry-After is the least time the server asks for, so it can lengthen a pause
but never shorten it. The backoff now advances on every failure, also one with
a hint, so repeated rate limits wait longer; a success still resets it.
@tee8z tee8z self-assigned this Oct 23, 2026
@tee8z
tee8z requested a review from thebrandonlucas October 23, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant