Plate № 30 · Routing · antifragile redirects
A pattern from the gf.cx specimen book
A link that outlives the host it points to
"Remove the coupling, not the component." A capability shouldn't depend on any single host being up — so move the host off the page and let a resolver find a live one at request time.
The problem
Some external resources have no stable home. A shadow library, a censored mirror, a rotating CDN — the canonical host keeps dying: takedowns pull one domain, the operators spin up another, a third comes and goes for days at a time. The moment you hard-code https://some-host.org/… into a page, you've written a link that is one takedown away from a dead end — and you won't know it broke until a reader hits it. Editing every page each time a mirror rotates doesn't scale, and it couples your content to a host you don't control.
The shape
Don't put the host on the page at all. The page links to a small resolver on your own domain — a request-time function — and passes only the intent (a query, a format). At request time the resolver probes a preference-ordered mirror set and 302-redirects to the first host that answers. The page carries no host; it self-heals as mirrors rotate. Widen the mirror set once and every page that links to the resolver inherits the new host — the system gets stronger as you add mirrors, which is what makes it antifragile rather than merely robust.
The reference implementation is growth.gf.cx/functions/go/annas.ts — a Cloudflare Pages Function at route /go/annas?q=<query>&ext=<pdf|epub>. It resolves Anna's Archive, whose domains (annas-archive.org / .se / .pk / .li / .gs / .gl) rotate constantly under takedown pressure. A page links /go/annas?q=Title+Author&ext=epub and never names a mirror.
The probe
A host is "alive" if it answers at all. The probe is a HEAD request with a short AbortController timeout: any HTTP response — even a 403 or 405 — means the host resolves and is serving, so it counts as alive. Only a network error, DNS failure, or timeout counts as dead. This is deliberately generous: a mirror that 403s a bare HEAD still serves the search page to a real browser, so treating it as dead would discard a working host.
const MIRRORS = ["pk", "li", "gs", "se", "org", "gl"];
const DEFAULT_TLD = "pk"; // last-resort target if every probe fails
const PROBE_TIMEOUT_MS = 2500;
async function isAlive(tld: string): Promise<boolean> {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), PROBE_TIMEOUT_MS);
try {
await fetch(`https://annas-archive.${tld}/`, {
method: "HEAD", signal: ctrl.signal, redirect: "manual",
});
return true; // any response = alive
} catch {
return false; // network error / DNS / timeout = dead
} finally {
clearTimeout(timer);
}
}
Mirrors are probed in preference order — currently-live hosts first (pk, li, gs), the official ones (se, org) as fallback — and DEFAULT_TLD is the last-resort target if every probe fails, so the resolver always produces a destination rather than an error.
Caching & the no-store redirect
Probing six hosts on every hit would be slow, so the resolved host is cached ~10 minutes per colo via the Cache API (caches.default with a synthetic key), written with context.waitUntil so it never blocks the response. The one subtlety: the 302 itself is sent with cache-control: no-store. That means the client always comes back through the resolver and can never pin a host that later dies — only the resolver's internal choice is cached, and only briefly.
const cache = caches.default;
const cacheKey = new Request(MIRROR_CACHE_KEY);
let tld = "";
const cached = await cache.match(cacheKey);
if (cached) tld = (await cached.text()).trim();
if (!MIRRORS.includes(tld)) {
tld = await resolveMirror(); // probe in preference order
const store = new Response(tld, {
headers: { "cache-control": `max-age=${MIRROR_TTL_S}` }, // ~600s
});
waitUntil(cache.put(cacheKey, store));
}
// no-store so the client always re-resolves through us (never pins a host):
return new Response(null, {
status: 302,
headers: { location: target, "cache-control": "no-store" },
});
Safe by construction
A request-time redirector is an open-redirect waiting to happen if you let the input choose the host. This one can't:
- Host is never user-supplied. The resolver only ever redirects within the
annas-archive.*family, for a whitelisted tld from the fixedMIRRORSlist. There is no path by which a query parameter becomes the destination host. - Inputs are bounded.
qis length-capped (200 chars) andextis a strictpdf|epubwhitelist — anything else is dropped, not passed through. - All methods resolve. The function is exported as
onRequest(not justonRequestGet), so aHEADlink-check and aGETboth get the same302— a link-checker sees a live redirect, not a 405.
Companion lesson · search by title, not ISBN
A practical footnote for shadow-library search specifically: search by title + author, not ISBN. New books aren't ISBN-indexed for months, and per-format results can be empty in the early window — that's expected, not a broken link. The title search self-populates as uploads land, so a title query degrades gracefully where an ISBN query would look dead.
Where it applies
- Shadow libraries — Anna's Archive, and sci-hub-style hosts whose domains rotate under takedown pressure.
- Censored mirrors — any resource served from a rotating mirror set because its canonical host is blocked or pulled in some jurisdictions.
- Rotating CDNs / ephemeral hosts — anything where the live host is one of several and changes without notice.
- Generally: any external resource whose canonical host rotates or goes dark. The page should carry the intent; a resolver on your own domain should carry the (mutable) host.
When it breaks
- Probing too strictly. If you treat a
403/405as dead, you discard hosts that serve real users fine — a bare HEAD is often challenged even when the site works. Alive = any HTTP response; dead = only network/DNS/timeout. - Caching the
302at the client. Drop theno-storeand a browser will pin the redirect to a host that later dies — reintroducing exactly the coupling the pattern removes. The resolver's internal choice may cache; the client-facing redirect must not. - Letting input pick the host. The instant a parameter can influence the destination host, it's an open redirect. Keep the host set fixed and whitelisted; only the query and format come from the URL.
Reference
- Reference impl
growth.gf.cx/functions/go/annas.ts— a Cloudflare Pages Function at/go/annas?q=<query>&ext=<pdf|epub>. ProbesMIRRORS = ["pk","li","gs","se","org","gl"]in order (HEAD + 2.5s timeout), caches the live host ~10 min per colo via the Cache API, and302s withcache-control: no-store. Shipped & live.- Principle
- The ops baseline's remove the coupling, not the component — antifragility by decoupling from any single dependency — turned on an outbound link instead of an unattended job. Kin to the antifragile contact form (same principle, on an inbound form) and the memorable-URL canonical redirect (same mechanism, opposite intent — a permanent home, not a rotating one).
- Rule
- The page carries the intent; a resolver on your own domain carries the mutable host. Widen the mirror set and every linking page self-heals — stronger as you add mirrors.