resolveRetryBackoff function
Resolves ONE attempt's backoff delay (issue #1398 AC2):
- a parseable delta-seconds
Retry-Afterwins over the ladder for that attempt (RetryDelaySource.server), hard-capped at retryBackoffCeiling (RetryDelaySource.clamp — the trace names the advertised value the ceiling overrode); Retry-After: 0counts as absent (E2 — the server says "go now", which is what the ladder's short step already does);- a malformed header (non-numeric, negative, or the HTTP-date form)
falls back to the ladder and flags
RetryBackoffDecision .malformedHeaderso the trace line can note it (E3).
Pure: no sleeping, no clock — the caller performs the wait (fake-clock tests assert the DECISION).
Implementation
RetryBackoffDecision resolveRetryBackoff({
String? retryAfter,
required int attempt,
Duration? ladderStep,
}) {
final step = ladderStep ?? retryBackoffLadderStep(attempt);
final raw = retryAfter?.trim() ?? '';
if (raw.isEmpty) {
return RetryBackoffDecision(delay: step, source: RetryDelaySource.ladder);
}
final seconds = int.tryParse(raw);
if (seconds == null || seconds < 0) {
// E3: non-numeric / negative — and the HTTP-date form too (the stall
// ladder honors delta-seconds only; codex's retry_delay is
// delta-seconds as well).
return RetryBackoffDecision(
delay: step,
source: RetryDelaySource.ladder,
malformedHeader: true,
rawHeader: raw,
);
}
if (seconds == 0) {
// E2: "0" is absence — the ladder applies.
return RetryBackoffDecision(delay: step, source: RetryDelaySource.ladder);
}
final advertised = Duration(seconds: seconds);
if (advertised > retryBackoffCeiling) {
return RetryBackoffDecision(
delay: retryBackoffCeiling,
source: RetryDelaySource.clamp,
advertised: advertised,
);
}
return RetryBackoffDecision(
delay: advertised,
source: RetryDelaySource.server,
advertised: advertised,
);
}