oven-sh/bun · error · Error

no zip EOCD in ${url}

Error message

no zip EOCD in ${url}

What it means

zipBinarySize() scans the fetched 64 KB tail backwards for the zip End-Of-Central-Directory signature 0x06054b50 (covering the maximum 64 KB zip comment). This error means no EOCD was found — the tail does not look like the end of any zip file.

Source

Thrown at scripts/binary-size.ts:339

// (`<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}`);

  let size = 0;
  while (p + 46 <= eocd && dv.getUint32(p, true) === 0x02014b50) {
    const uncompressed = dv.getUint32(p + 24, true);
    const nameLen = dv.getUint16(p + 28, true);
    const name = new TextDecoder().decode(buf.subarray(p + 46, p + 46 + nameLen));
    // The binary is the only non-directory entry; take the largest in case the
    // zip ever grows extra metadata files.
    if (!name.endsWith("/") && uncompressed > size) size = uncompressed;
    p += 46 + nameLen + dv.getUint16(p + 30, true) + dv.getUint16(p + 32, true);
  }
  if (size === 0) throw new Error(`no file entry in ${url}`);
  return size;
}

View on GitHub (pinned to 8c5296ac45)

Solutions

  1. curl the URL and inspect content-type plus the first bytes (a zip starts with PK\x03\x04)
  2. Verify the URL came from the release assets list and matches isBinaryZip
  3. Download the file fully and run unzip -l to confirm it is a valid zip
  4. If asset naming changed, update the isBinaryZip filter
Defensive patterns

Strategy: fallback

Validate before calling

// cheap sanity check before trusting the tail: a zip starts with PK\x03\x04
const head = await fetch(url, { headers: { Range: "bytes=0-3" } });
const magic = new Uint8Array(await head.arrayBuffer());
if (magic[0] !== 0x50 || magic[1] !== 0x4b) throw new Error(`not a zip: ${url}`);

Try / catch

try { return await zipBinarySize(url); }
catch (e) {
  if (/no zip EOCD/.test(e.message)) {
    return sizeFromFullZip(new Uint8Array(await (await fetch(url)).arrayBuffer()));
  }
  throw e;
}

Prevention

When it happens

Trigger: The response body is not actually a zip — an HTML/XML error page returned with a 2xx status from the CDN; content-length lied so the tail window misses the real end; or the object is truncated.

Common situations: CDN soft-error pages on stale URLs; an asset replaced by a redirect or pointer file; proxies returning partial content with 200.

Related errors


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