oven-sh/bun · error · Error
HEAD ${url}: ${head.status}
Error message
HEAD ${url}: ${head.status} What it means
zipBinarySize() in scripts/binary-size.ts issues a HEAD request against a release asset URL to learn content-length (needed to compute the tail Range window). This error means the HEAD returned non-2xx, so the size probe cannot proceed.
Source
Thrown at scripts/binary-size.ts:324
}
async function sizesFromZips(triplets: string[], urls: Map<string, string>): Promise<Sizes> {
const out: Sizes = {};
await Promise.all(
triplets.map(async t => {
out[t] = await zipBinarySize(urls.get(t)!);
}),
);
return out;
}
// Read the uncompressed size of the binary inside a release zip without
// downloading the whole archive. The central directory + EOCD live at the end
// of the file; a 64 KB Range request is more than enough for our two-entry
// (`<triplet>/` + `<triplet>/bun[.exe]`) zips.
async function zipBinarySize(url: string): Promise<number> {
const head = await fetch(url, { method: "HEAD" });
if (!head.ok) throw new Error(`HEAD ${url}: ${head.status}`);
const total = Number(head.headers.get("content-length"));
const tail = Math.min(65536, total);
const res = await fetch(url, { headers: { Range: `bytes=${total - tail}-${total - 1}` } });
if (!res.ok) throw new Error(`Range ${url}: ${res.status}`);
const buf = new Uint8Array(await res.arrayBuffer());
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
let eocd = -1;
for (let i = buf.length - 22; i >= Math.max(0, buf.length - 22 - 65535); i--) {
if (dv.getUint32(i, true) === 0x06054b50) {
eocd = i;
break;
}
}
if (eocd < 0) throw new Error(`no zip EOCD in ${url}`);
let p = dv.getUint32(eocd + 16, true) - (total - tail);
if (p < 0) throw new Error(`zip central directory not within tail for ${url}`);View on GitHub (pinned to 8c5296ac45)
Solutions
- Re-run — transient CDN errors usually clear
- Re-fetch the release asset lists so browser_download_url values are fresh instead of reusing cached ones
- Verify the asset still exists: curl -I the URL
- Check that the isBinaryZip pattern still matches current asset naming
Example fix
# before — cached release listing yields a dead URL
Error: HEAD https://objects.githubusercontent.com/...: 403
# after — resolve fresh asset URLs every run
const [latest, canary] = await Promise.all([gh("releases/latest"), gh("releases/tags/canary")]); Defensive patterns
Strategy: retry
Validate before calling
const probe = await fetch(url, { method: "HEAD" });
if (!probe.ok || !Number(probe.headers.get("content-length"))) {
throw new Error(`asset unreachable or size unknown: ${url}`);
} Try / catch
try { size = await zipBinarySize(url); }
catch (e) {
if (/^HEAD /.test(e.message)) { urls = await refreshAssetUrls(); continue; } // re-resolve and retry
throw e;
} Prevention
- Never cache browser_download_url across long-lived sessions
- HEAD before Range so a dead URL fails fast with a clear signal
When it happens
Trigger: A stale browser_download_url (they can expire or rotate when an asset is re-published), a CDN that rejects HEAD, or 404 because the release asset was renamed or removed since the listing was fetched.
Common situations: Comparing against a canary release that was re-published mid-run; asset naming changes (new triplet/musl/baseline variants) no longer matching isBinaryZip; transient CDN 5xx.
Related errors
- Range ${url}: ${res.status}
- github ${path}: ${res.status}
- github ${p}: ${r.status} ${r.statusText}
- no zip EOCD in ${url}
- zip central directory not within tail for ${url}
AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16).
Data as JSON: /api/errors/58547daa00a2c995.
Report an issue: GitHub.