oven-sh/bun · error · Error

zip central directory not within tail for ${url}

Error message

zip central directory not within tail for ${url}

What it means

zipBinarySize() found the EOCD, but the central-directory offset it records (read at EOCD+16, rebased by the tail start) is negative — the central directory begins earlier than the fetched 64 KB tail, so entry headers are not fully inside the buffer.

Source

Thrown at scripts/binary-size.ts:342

  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. Raise the tail window in zipBinarySize, e.g. fetch min(1 MB, total) instead of 64 KB
  2. Or fall back to downloading the whole zip when this throws
  3. Verify only the expected two-entry release zips are being probed (check the isBinaryZip filter)

Example fix

// before
const tail = Math.min(65536, total);

// after — central directories can exceed 64 KB
const tail = Math.min(1024 * 1024, total);
Defensive patterns

Strategy: fallback

Try / catch

try { return await zipBinarySize(url); }
catch (e) {
  if (/central directory not within tail/.test(e.message)) {
    return sizeFromFullZip(new Uint8Array(await (await fetch(url)).arrayBuffer()));
  }
  throw e;
}

Prevention

When it happens

Trigger: A release zip whose central directory (all entry headers plus names and extra fields) exceeds ~64 KB. The release zips here normally hold only two entries (the <triplet>/ directory and the bun binary), so any layout growth breaks the assumption.

Common situations: Release tooling adds metadata files or long entry names; a different artifact type accidentally matches isBinaryZip and is probed.

Related errors


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