oven-sh/bun · error · Error

Range ${url}: ${res.status}

Error message

Range ${url}: ${res.status}

What it means

After the HEAD probe, zipBinarySize() requests the last ~64 KB of the zip via a Range header (bytes=total-tail through total-1) to read the central directory. This error means that Range request returned non-2xx — the server refused or failed the partial download.

Source

Thrown at scripts/binary-size.ts:328

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

  let size = 0;
  while (p + 46 <= eocd && dv.getUint32(p, true) === 0x02014b50) {
    const uncompressed = dv.getUint32(p + 24, true);

View on GitHub (pinned to 8c5296ac45)

Solutions

  1. Re-run — signed-URL expiry between HEAD and Range usually resolves itself
  2. Ensure the HEAD actually returned a valid content-length before the Range is built
  3. As a manual fallback, download the whole zip and read the binary size locally
Defensive patterns

Strategy: fallback

Validate before calling

const total = Number(head.headers.get("content-length"));
if (!Number.isFinite(total) || total <= 0) {
  // Range math would be garbage; fall back to a full download
  return sizeFromFullZip(new Uint8Array(await (await fetch(url)).arrayBuffer()));
}

Try / catch

try {
  return await zipBinarySize(url);
} catch (e) {
  if (/^Range /.test(e.message)) {
    const buf = new Uint8Array(await (await fetch(url)).arrayBuffer()); // full-download fallback
    return sizeFromFullZip(buf);
  }
  throw e;
}

Prevention

When it happens

Trigger: A CDN or proxy that ignores or rejects Range on signed URLs (416 when byte math is off, 403 when the second signed request no longer validates), or the object changed between HEAD and Range so length/signature no longer match.

Common situations: GitHub asset CDN expiring the signed URL between the two requests; corporate proxies stripping Range; a missing/zero content-length on the HEAD making the Range header malformed.

Related errors


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