Reconcile the Retry-After rule for a relayed upstream status across S13, S15, S16 and S31
Problem
Two Retry-After claims in the merged specs need one amendment across four documents. Both surfaced in review of !1678 (merged), which adds remote.RetryAfterSeconds and deliberately states no answer for either.
1. A 503 that relays an upstream status has no agreed Retry-After
The merged documents disagree:
docs/specs/S13-virtual-remote-foundation.md:431says a slice relaying a status the upstream itself produced relays that response's ownRetry-Afterwhere it carried one, and that the fixed value covers only the responses the three named verdicts produce.docs/specs/S31-npm-virtual.md:957and:982repeat that for theWinnerFetchFailedrow and add that the fixed health-interval value is not added. TheResolutionsbullet at:1201-1211gives the reason: the value is a claim about when an upstream's health status can next change, so a503reporting no upstream condition has no business quoting it.docs/specs/S15-npm-remote.md:753requiresRetry-Afteron every503 upstream_unavailable. The upstream-5xxrow at:748propagates a non-500upstream5xxwith theupstream_unavailablecode, so a propagated upstream503is one of those responses.docs/specs/S16-container-remote.md:712and:941require the header on every503 UNAVAILABLE, a propagated or remapped upstream5xxincluded, and say this handler supplies its own value where the upstream sent none.
Relaying the upstream's own value is not possible today. forwardableHeaders in internal/remote/operations.go is a closed allowlist of Content-Type, ETag, Cache-Control and Last-Modified, so FetchResult.SafeHeaders and ServeResult.SafeHeaders never expose an upstream Retry-After. docs/dev/api-style.md:54 says the same thing from the other side: propagated responses carry no upstream headers.
A renderer for a relayed 503 therefore has no value it can send that satisfies all four documents.
2. The reason the fixed value rests on is broader than what holds
docs/specs/S13-virtual-remote-foundation.md:428 and :881, docs/specs/S31-npm-virtual.md:977-980 and :1210-1211, and the Proposal in #609 (closed) all say the sweep interval is the earliest point at which the status the client is retrying against can change. The claim that holds is narrower: a client honoring the header retries no sooner than the next sweep is due. The next sweep is due at most one interval away, a manual probe changes a status on demand, and the UpstreamUnavailable verdict answers a live transport failure rather than a stored status.
The two S31 passages carry weight, because that reason is S31's stated argument for refusing the value on the WinnerFetchFailed row. Narrowing it weakens the argument, which is why the two items belong in one amendment rather than two.
Proposal
One spec MR amending docs/specs/S13-virtual-remote-foundation.md, docs/specs/S15-npm-remote.md, docs/specs/S16-container-remote.md and docs/specs/S31-npm-virtual.md together: decide item 1, and correct item 2 in the four passages it touches.
Item 1 needs a design decision first. Two shapes it could take:
- Widen the relay allowlist so an error response can carry the upstream's own
Retry-After, and keep S31's rule that nothing is added where the upstream sent none. S15 and S16 then say what a relayed503without an upstream value carries, instead of requiring a value with no source. - Keep the allowlist closed and fix a value for a relayed
503that is not the health-sweep interval, so no response quotes a health claim it is not in a position to make.
Scope
- The design decision on item 1.
- One spec MR across the four documents, covering both items.
Notes
Nothing in the service reads either claim yet. remote.RetryAfterSeconds has no caller while the slice and fill-handler MRs that render these 503s are open; the first of those to merge is the point where the decision becomes visible to a client, so the amendment is worth landing before it.
The Retry-After value for the three verdicts remote.RetryAfterSeconds covers is settled and is not in question here: !1502 (merged) fixed it in S13, and !1678 (merged) derives it.
Related to #609 (closed) and !1678 (merged).