A fetch that does not follow redirects verifies the redirect body, not the resource
A redirect is a successful response. A fetch not told to follow one does not fail, warn or return nothing - it returns the redirect's own body, which on most hosting platforms is a short placeholder. Every downstream layer then does exactly what it was written to do, against the wrong bytes. The direction is what makes it dangerous: verification steps built on a fetch are usually looking for the ABSENCE of something - an empty diff, a grep with no hits, a checksum comparison - and a placeholder body satisfies every one of those tests.
Resurfaces when
- about to build a feature on a URL pattern you just checked answers successfully
- deciding whether a resource exists by requesting only its headers
- constructing an asset address from an id because the record returned an id rather than a URL
- about to compare a local copy of a file against the deployed original it was copied from
- writing a check that greps a deployed page or asset for a string that must or must not be there
- scripting anything against a hostname that is an alias, a vanity domain, or a platform-assigned deployment URL
- copying an asset from another deployment into this repo and wanting proof the copy is current
- a diff against a fetched URL reports every local line as an addition
- a fetched file reads as empty, or as a few words that are not the format expected
- a forbidden-string scan over a remote page suddenly returns zero hits
- a check that passed for months keeps passing after the thing it watches was renamed or moved
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
A redirect is a successful response. A fetch that is not told to follow one does not fail, does
not warn, and does not return nothing — it returns the redirect's own body, which for most hosting
platforms is a short human-readable placeholder like Redirecting.... Every layer downstream then
does exactly what it was written to do, against the wrong bytes.
The direction of the failure is what makes it worth a lesson. Verification steps built on a fetch are almost always looking for the absence of something: a diff that should be empty, a grep for a forbidden string that should return no hits, a checksum comparison that should match. Feed any of those a 40-byte placeholder and it reports the reassuring answer. The check does not break loudly; it degrades into a check of nothing that still prints a pass.
There is a second reason this survives review. The hostname in the script is usually correct — it is the name the project publishes, prints on its own share cards, and links from its own docs. The alias was added later, by someone reorganising a deployment rather than changing an address, and nothing about reading the script reveals that a redirect is now in the path. The URL a project calls its own and the URL that serves its bytes are allowed to differ, and only the fetch knows.
The failure that taught it
A committed copy of another deployment's icon had gone stale — the source had been redesigned and the copy still served the retired mark. The fix was to re-pull it, and the verification was the obvious one: fetch the live file, strip comments from both sides, diff them, and print a success line if the geometry matched.
The diff reported that every line of the local file was an addition, which is what a diff prints
when one side is empty. The empty side was the fetched one. The deployment had been aliased at some
point and now answered the published hostname with a 307 to a differently-spelled one, so the fetch
had compared the local file against the word Redirecting....
That specific run failed in the visible direction and cost a minute. The same command written one
step differently would not have. Had it been grep -q '<circle' on the fetched body, it would have
reported the file as lacking the expected shape — a wrong conclusion about the source. Had it been
a denylist scan asserting a fetched page contains no private identifiers, it would have returned
zero hits and cleared a page nobody actually looked at. Same missing flag, same silence, and in
those two shapes nothing on screen suggests the resource was never retrieved.
The second instance: the status alone, with no body at all
The first instance compared against the redirect's body. The second never asked for a body.
Building a card that needed an organisation's logo, the record returned an image id rather than
an address, so the address had to be constructed from a documented id-to-asset route. That
construction was checked the quick way — a headers-only request, printing the status. It answered
302. A redirect is a successful response, the URL was therefore declared good, and the feature was
built on it.
The redirect went to the platform's 404 page. The asset does not exist at that route for these records at all. On a device the logo rendered as an empty box, which reads as a slow image or a missing upload rather than as an address that was never valid.
Two things generalise beyond the first instance.
"Does this resource exist" is not answerable by a status code when redirects are in play. A 3xx
says only that something answered and is pointing elsewhere; it says nothing about whether the
destination is the resource, an error page, or a login wall. The check has to reach the destination
and look at what arrived. One flag apart: -o /dev/null -w '%{http_code}' answered 302 and
-L -w '%{http_code} %{url_effective}' answered 404 …/404/.
A headers-only probe is the shape most likely to be trusted, because it looks careful — it is deliberate, it is cheap, it prints a number. The care is real and aimed at the wrong question. When the question is existence rather than reachability, the body is the evidence and the status is the decoy.
There is a construction-time tell worth recognising on its own. Being handed an id where a URL was expected means some layer that resolves ids to addresses is not in this path, and reconstructing that address by hand is a guess about a contract you have not read. A sibling endpoint returning the same record with the address resolved is usually cheaper than the reconstruction, and it is verified by construction rather than by probe.
How to apply it
Pass the redirect-following flag on every fetch in a verification path, not only where a
redirect is expected. curl needs -L; a fetch in a script defaults to following and a fetch
through some HTTP clients and proxies does not — check the client, do not assume the default.
Assert on the shape of what came back before comparing it. One line — the payload is non-empty
and contains a marker the format requires (<svg, <!doctype html>, a leading {) — converts
this entire class from silent to loud. A verification step that never states what it expects to
receive cannot tell "matched nothing" from "fetched nothing".
Treat a hostname you do not administer as a redirect risk permanently. Platform deployment
URLs, vanity domains and project aliases are reorganised by people who are not thinking about your
script, and the redirect they add is invisible in a browser. If a check is worth having, print the
effective URL it actually read (curl -w '%{url_effective}') so a rename shows up in the output
rather than in a conclusion.
When a fetched-resource check reports the comfortable answer, re-run it with the resource printed. This is the general rule applied to one input: a clean result from a check that may never have looked is indistinguishable from a clean result from a check that did.
Never accept a 3xx as proof a resource exists. If the question is existence rather than reachability, follow the redirect and print where it landed. A status-only probe is the version of this mistake that feels most rigorous, and it is the one that ships a broken address into a feature.
When a record hands you an id where you expected an address, look for the endpoint that resolves it before building the address yourself. The reconstruction is a guess about a contract you have not read; a sibling route returning the same record with the address already resolved usually costs the same single request and cannot be wrong in this way.
Where this claim comes from
- Lesson
A fetch that does not follow redirects verifies the redirect body, not the resource
- Distillationmechanism stated
Written from the mechanism, not the incident — which is what lets it transfer to code sharing nothing with the original.
- Scar2 occurrences
2 separate failures, recorded independently at the time. A clustering pass found they shared one cause.
- Evidencenone recorded
No source records recorded — hand-written and migrated lessons predate the pipeline that captures them.
What happened when it was used
- 12
- 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.