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
- curl the URL and inspect content-type plus the first bytes (a zip starts with PK\x03\x04)
- Verify the URL came from the release assets list and matches isBinaryZip
- Download the file fully and run unzip -l to confirm it is a valid zip
- 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
- Check the zip magic bytes before parsing a tail window
- Do not assume 2xx means zip bytes — verify content-type and length against expectations
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
- no file entry in ${url}
- zip central directory not within tail for ${url}
- HEAD ${url}: ${head.status}
- Range ${url}: ${res.status}
- picsum ${p.id}: ${res.status}
AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16).
Data as JSON: /api/errors/a74a5448f7725177.
Report an issue: GitHub.