SPEC §6: decide which statuses honour Retry-After, and how each loop composes it
Decides what #775 asked for: which statuses honour Retry-After, and how each delay loop composes the value with its own delay.
Scope note. This PR previously also carried SPEC §9's peer-derived-text rule. That half is now #802 (draft). §6 is settled — a decided status rule, a composition table walked back across 11 branches, a precise definition of "retry", the representability tiers, and zero open findings. §9 moved its central boundary three times in one review cycle and the fourth candidate's premise failed a spot check, so it is a section still being designed rather than one being reviewed. Splitting lets the settled half ship.
The status rule
A parsed Retry-After is honoured at any status a retry is already going to happen at. No status gate of its own — honouring is derived from retry eligibility, so it needs no amendment when a retry set grows. This is what §7's algorithm has always spelled: step 3h carries no status test and is reachable only past step 3f's declared-set check. The five SDKs' 429-only gates are narrower than the algorithm they implement.
"Retry" is now defined
Leaving that word to intuition cost three review rounds, each finding another repeating loop the rule appeared to reach and the code deliberately did not.
A retry is the re-issue of a request whose previous attempt produced no answer — the transport failed, or the origin declined to serve it with a status the loop declares retryable. Re-issuing because the answer was "not yet" is a poll.
§16's authorization_pending and slow_down are 4xx protocol answers to a completion poll, so they are outside by definition, not by exception — the next loop is in or out by the criterion rather than by someone noticing. Both clauses carry weight: §4's 401 replay re-issues after a refusal and stays out only because no loop declares 401 retryable.
Walked all 11 delay-bearing branches back through the definition — §7, §14, §16's four, §23's five, §4 — and every one lands on the side its code already implements, with no carve-outs.
Composition is per-loop
Separated from honouring, with no default, so a delay loop added later is under-specified until it states its own rather than silently inheriting one that may not fit.
| Loop | Composition |
|---|---|
| §7 generated retry | replaces |
| §14 hop 1 | replaces |
| §16 device poll | max(interval, retryAfter) |
§23 reconnect backoff | floors the jitter draw |
§23 poll-retry | replaces — waited exactly |
§23 needs two rows because its timers genuinely differ — a 1s header against a 50s reconnect draw waits 50s, while the same header on a poll-retry waits 1s. That is the sharpest evidence a single rule would not have fitted.
Representability
Two tiers: unrepresentable in the parser's own numeric type is malformed; representable but beyond what the host can schedule saturates. Per-SDK widths recorded, Go's ceiling corrected to the portable 2,147,483,647s, and the date-form inventory rewritten per parser after probing found the previous split backwards (Ruby accepts all three RFC 7231 forms, Python two; only TypeScript and Swift are IMF-fixdate-only, and generated Go accepts no date form at all).
No behaviour changes
Every row and tier records what the code already does. Divergences are marked [CONFLICT] and tracked in #775, #798 and #799.
Tracks #775, which stays open until the status convergence it inventories lands — this PR delegates the per-SDK inventory and three clauses to it.