The first address in a list of mirrors is not the address, and a downstream fallback handler can swallow the refusal that proves it
An API hands back several addresses for one file because they are mirrors with different hosts and different acceptance rules; a client that hardcodes index 0 inherits whichever host the server listed first, and when that host gates on a request header the client does not send, the refusal lands on an arbitrary subset of items and reads as a content-dependent bug.
Resurfaces when
- about to download from an API that returns a list of URLs for one file
- a response field named UrlList or url_list or mirrors
- taking the first element of a list of alternative addresses
- writing a downloader against a CDN
- a CDN answers 403 to a bare request
- downloads work for some items or qualities and fail for others
- a link may have expired error on a fresh link
- a fallback branch logs the wrong cause for a failure
- a consumer coroutine catches the exception a producer put on its future
- a join or merge step reports failure when the fetch failed
- adding a Referer header to a media request
The lesson’s retrieval contract, weighted three times heavier than its body. A lesson that states when it applies doesn’t need a semantic search to find it.
The lesson
When an API returns a list of addresses for one file, the list is the contract, not its first element. The entries are mirrors on different hosts, and the hosts do not all accept the same request: one may gate on a header the client never sends and answer 403, while its sibling serves the same bytes to the bare request. Which host is listed first is the server's choice per item, so a client that hardcodes index 0 fails on an arbitrary subset of items and the failure looks content-dependent — "this resolution works, that one does not" — when nothing about the content is involved.
A second, independent mistake hid the first. The download and the join ran as producer and consumer, with the consumer awaiting a per-item future. When a download failed, the failure surfaced from the consumer's await, and the consumer's own catch-all — there to route a join incompatibility to a slower path — caught it, logged "join failed, re-encoding", and marked the join as the culprit. The only record of the real cause was the wrapped exception in a warning line nobody was reading.
The failure that taught it
A mobile client for a short-video service downloaded episodes and joined them on device. Users reported that one quality rung downloaded fine and every other rung failed with "the link may have expired". The obvious theories — the quality picker selecting the wrong source, a size mismatch, a format check — were all wrong; the picker copied every field faithfully.
Reproducing the request from a laptop showed that the first address in every stream's list answered 403 regardless of rung, and the second address answered 206 for all of them. The difference was the host: one demanded a site Referer, the other did not. The client sent only a User-Agent and only ever read index 0. On the device the "working" rung had merely happened to land on the permissive host.
The fix was three things: send the Referer the real client sends, treat the list as mirrors and move to the next one on a definite refusal, and wrap the awaited producer failure so the consumer's fallback handler cannot claim it.
How to apply it
- Before writing a downloader against an API that returns a list of URLs, read the list as mirrors. Loop over it on a 4xx; retry the same address only on a transient failure.
- Send the headers the first-party client sends. A Referer, an identifying header, a cookie: a gate on any of them makes a bare request a different request. See the related lesson on reproductions that omit the client's headers.
- Where a consumer awaits a producer's future inside its own try/catch, rethrow anything that came out of the await before the catch-all runs. A fallback handler that catches a failure it cannot fix will mislabel it.
- When a symptom sorts by a content property, check whether the sort is really by server-side routing — which host, which region, which index — before theorising about the content.
Carries a runnable check
It names a grep-able signature — the shape this failure takes on sight. Prose doesn’t prevent recurrence; executable checks do. This one runs today, against every edit, as signature-scan.
Where this claim comes from
- Lessonmedium confidence
The first address in a list of mirrors is not the address, and a downstream fallback handler can swallow the refusal that proves it
- Distillationmechanism stated
Written from the mechanism, not the incident — which is what lets it transfer to code sharing nothing with the original.
- Scar1 occurrence
One recorded failure — weaker evidence, and ranked accordingly rather than presented as settled.
- Evidencenone recorded
No source records recorded — hand-written and migrated lessons predate the pipeline that captures them.
What happened when it was used
- 3
- retrieved
- 0
- acted on
- 0
- held up
- 0
- did not hold up
Retrieved but never acted on — a demotion, not a neutral result. A lesson that keeps winning the search and never changes a decision is noise.