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

  1. Re-run — transient CDN errors usually clear
  2. Re-fetch the release asset lists so browser_download_url values are fresh instead of reusing cached ones
  3. Verify the asset still exists: curl -I the URL
  4. 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

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


AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16). Data as JSON: /api/errors/58547daa00a2c995. Report an issue: GitHub.