feat(remote): serve taxonomy, transport marker, and upstream doer adapter (S13 Step 11, part 1/3)
📦 What this MR does
Part 1 of 3 of S13 virtual/remote foundation — Step 11: standalone remote request.
This step is split into 3 stacked MRs to keep each within the review size limit (~800 reviewable LoC). Each part targets the previous one (part 1 targets main); review and merge proceed bottom-up.
This part lands the shared foundations the standalone flow (part 2/3) composes:
ServeResult/ServeSource(serve.go, new): the serve-outcome taxonomy for the standalone flow (the Step 12 resolver ships its own result type ininternal/virtual— see !1286 (merged)/!1287 (merged)), withSafeHeaderssharingFetchResult's fail-closed allowlist through the extractedsafeHeadersFrom.ServeSourcegives the cache-fallback serve its own member,ServeFromCacheDegraded: it shares everyServeResultfield rule withServeFromCacheand differs only in the claim, so a log or metric keyed on the source separates a working cache from a failing upstream, and a handler can branch on it for a staleness header. Nothing in this part sets it — part 2'sStandalone.Servewill, on itsserveOnFetchErrorpath.errUpstreamTransport(fetch.go): Fetch marks every failure of an issued upstream GET with a transport sentinel, at two sites —fetchOnce's post-send wrap, andcacheFillTee's discard site for a mid-transfer body read, which the wrap site cannot reach becausestreamAndFillhas already returned. Cache-fallback eligibility is therefore positive and fail-closed:fallbackEligible(with its 21-row partition test) requires the marker, and a local error introduced anywhere else — a staging-write failure included — can never become fallback-eligible by omission. Because the second site marks every non-EOF body read, the two read-time body caps arrive marked and are excluded by explicit predicate instead: a cap breach is a deterministic, operator-owned rejection, and every retry breaches the same cap, so answering it from cache would pin clients to an aging copy and hide the misconfiguration. A Fetch-level test drives a body past a cap and asserts marker, sentinel transparency and ineligibility together, since the marking and the predicate live in different files. Fetch now also releases a partial response a doer returns alongside its error.followerLeaderError(singleflight.go): a coalesced follower classifies for cache fallback exactly as its leader did, and the follower-facing wrap carrieserrUpstreamTransportexactly as far as that symmetry requires. A dial failure or connection reset travels transparently, marker and sentinels intact, rather than reaching followers labelled as a cancellation. The two context errors are opposite cases and are handled as such: a markedcontext.Canceledis unambiguously someone hanging up, whichfallbackEligibleexcludes for the leader, so the marker is dropped rather than forwarded — carrying it across left every follower fallback-eligible while the leader correctly hard-failed, serving stale bytes against a reachable upstream. A markedcontext.DeadlineExceededcannot be told from therequest_total_timeoutthe upstream client applies to an unresponsive upstream, so it keeps the marker and drops theErrLeaderCanceledclaim, whose contract promises the outcome was not an upstream or transport failure and invites re-issuing the Fetch.logDegradedInfra(health.go): the shared degrade-not-abort logging helper (DEBUG on the caller's own cancellation, WARN otherwise), refactored out of the health monitor's counter-error logging for the standalone health gate to reuse once part 2 lands.upstreamhttp.Doer(new): the first productionremote.UpstreamDoeradapter over the security-hardenedupstreamhttp.Client, bridgingmaxBodyBytestoWithMaxBody.remotetest.AssertUpstreamDoerConformance(new): the shared seam-conformance assertion proving the twoUpstreamDoerpromises (nil error carries a non-nil body; the caller's cancellation stayserrors.Is-visible through a realcontext.CancelFunc), plusremotetest.LoopbackClientConfigas the one shared loopback client test policy (theupstreamhttpclient tests now delegate to it).no-remotetest-in-prod(.golangci.yaml): a depguard rule keeping a non-test file from importinginternal/remote/remotetest, whose loopback carve-out andtestingdependency must not reach a binary, with a negative-control probe ininternal/jobsand an entry in thedepguard:negative-controlassertion loop.
📖 References
📐 Plan-files divergence
The plan's Step 11 Files list is kept current in-tree and now enumerates all twenty-three files this branch touches; the divergence paragraph beneath it records why the list grew from the five paths first planned. In short: the serve-outcome taxonomy split out of standalone.go, the safeHeadersFrom/logDegradedInfra/errUpstreamTransport extractions are shared seams the review rounds settled on, the shared loopback config moved out of client_test.go, and review added the follower-error correction, the no-remotetest-in-prod rule with its probe and CI assertion, and a CLAUDE.md convention.
⌛ Work item 269
The adapter ships over Client.Do as-is: the unconditional request_total_timeout bounds the whole fetch, including a client-paced fill (document-and-defer, recorded in the Doer doc comment). Ratifying that bound is not a closure of the underlying problem — the progress-based read deadline stays open on work item 269.
🧪 E2E scenario catalog
No scenario added or affected: this is service-layer composition with no HTTP endpoint. The format handlers that expose the standalone flow will add the client-visible scenarios.
:bricks: Stacked MRs (review/merge bottom-up)
- feat(remote): serve taxonomy, transport marker,... (!1290 - merged) • David Fernandez • 19.3
👈 - feat(remote): standalone remote request flow (S... (!1291 - merged) • David Fernandez, Dzmitry (Dima) Meshcharakou • 19.3
- fix(remote): standalone serve suite and upstrea... (!1292 - merged) • David Fernandez, Dzmitry (Dima) Meshcharakou • 19.3
Related to #332 (closed)