@@ -183,16 +183,24 @@ func shouldRetry(resp *http.Response, err error) bool {
183183 return false
184184}
185185
186- // backoffDelay honours `Retry-After` when set, otherwise applies exponential
187- // backoff with jitter so a thundering herd doesn't synchronise.
186+ // backoffDelay applies exponential backoff with jitter so a thundering herd
187+ // doesn't synchronise, and treats `Retry-After` as a lower bound rather than an
188+ // override: the server hint is honoured only when it exceeds our own backoff,
189+ // and is clamped to maxBackoff. This keeps `Retry-After: 0` (e.g. a fleet-wide
190+ // rate-limit reset) from collapsing into a zero-delay retry storm, and stops a
191+ // hostile or misconfigured `Retry-After: 999999` from parking the CLI for days.
192+ // Mirrors twoadm-cli internal/httpx (see INF-1314).
188193func backoffDelay (attempt int , resp * http.Response ) time.Duration {
189- if resp != nil {
190- if v := resp .Header .Get ("Retry-After" ); v != "" {
191- if secs , err := strconv .Atoi (strings .TrimSpace (v )); err == nil && secs >= 0 {
192- return time .Duration (secs ) * time .Second
193- }
194- }
194+ d := jitteredBackoff (attempt )
195+ if hint := retryAfter (resp ); hint > d {
196+ d = hint
195197 }
198+ return d
199+ }
200+
201+ // jitteredBackoff returns the exponential backoff for `attempt` (0-based) with
202+ // [0, 25%) additive jitter, capped at maxBackoff.
203+ func jitteredBackoff (attempt int ) time.Duration {
196204 // Clamp attempt so the shift below can't overflow time.Duration on
197205 // pathological MaxRetries values. baseBackoff << 6 = 16s, already
198206 // past the cap, so any attempt ≥ 6 lands on maxBackoff anyway.
@@ -209,3 +217,25 @@ func backoffDelay(attempt int, resp *http.Response) time.Duration {
209217 }
210218 return d + time .Duration (rand .Int64N (jitter ))
211219}
220+
221+ // retryAfter parses the `Retry-After` header (seconds form), clamped to
222+ // maxBackoff. Returns 0 when the header is absent, negative, or unparseable so
223+ // the caller falls back to jittered exponential backoff.
224+ func retryAfter (resp * http.Response ) time.Duration {
225+ if resp == nil {
226+ return 0
227+ }
228+ v := strings .TrimSpace (resp .Header .Get ("Retry-After" ))
229+ if v == "" {
230+ return 0
231+ }
232+ secs , err := strconv .Atoi (v )
233+ if err != nil || secs < 0 {
234+ return 0
235+ }
236+ d := time .Duration (secs ) * time .Second
237+ if d > maxBackoff {
238+ return maxBackoff
239+ }
240+ return d
241+ }
0 commit comments