JuliusBrussee/caveman · error · Error
binary download failed: HTTP
Error message
binary download failed: HTTP ${response.status} What it means
The binary-installer fetches a release artifact from a remote URL via `asset()`, which performs an HTTP GET with an AbortSignal timeout. When the server responds with a non-2xx status (`response.ok` is false), the installer refuses to continue and throws this error carrying the numeric HTTP status. It exists so a failed download surfaces the server's status code instead of a generic fetch failure.
Solutions
- Check the HTTP status in the message and curl the exact release URL to confirm what the server returns.
- If 404: verify the release tag/version the installer is pointed at still exists and the artifact name is correct.
- If 403/429: wait out the rate limit or provide authentication (GITHUB_TOKEN / proxy credentials) for the download.
- If 5xx: retry after a short delay; the failure is server-side.
- As a workaround, install the binary manually and point the envVar (e.g. CAVEMAN_*_BIN) at it so `ensureBinary` short-circuits before any download.
Example fix
// before (fails with HTTP status)
await ensureBinary({ name: "ripgrep", envVar: "CAVEMAN_RIPGREP_BIN" });
// after: pin a verified binary or pre-seed the env var
if (!process.env.CAVEMAN_RIPGREP_BIN) process.env.CAVEMAN_RIPGREP_BIN = "/usr/local/bin/rg";
await ensureBinary({ name: "ripgrep", envVar: "CAVEMAN_RIPGREP_BIN" }); Defensive patterns
Strategy: try-catch
Validate before calling
// optional pre-flight reachability check
const res = await fetch(releaseUrl, { method: "HEAD" });
if (!res.ok) throw new Error(`release unavailable: HTTP ${res.status}`); Try / catch
try {
await ensureBinary({ name, envVar });
} catch (e) {
if (String(e.message).includes("HTTP ")) {
const status = Number(e.message.match(/HTTP (\d+)/)?.[1]);
if (status === 429 || status === 403) await backoffAndRetry();
else throw e;
} else throw e;
} Prevention
- Pin a known-good release version instead of following a moving latest tag.
- Use authenticated requests where possible to avoid GitHub rate limits.
- Pre-seed the envVar with a locally installed binary in CI/offline environments.
- Monitor the release feed for deleted/renamed tags.
When it happens
Trigger: Calling `ensureBinary({ name, envVar })` (directly or through the download flow) when `asset(url, timeout)` receives an HTTP response with status >= 400 — e.g. the release URL 404s because the version/tag doesn't exist, a proxy returns 403/407, or the CDN rate-limits with 429. Also reached from `download()` via its internal `asset()` call.
Common situations: Wrong or stale release base URL configured (CAVEMAN release feed moved or tag deleted); corporate proxy blocking github releases (403); GitHub rate limiting unauthenticated downloads (429/403); transient 5xx from the CDN; offline VPN environment returning an error page status.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- awscreds: sts assume role with web identity failed
- binary download failed
- binary download failed: response body missing
- cave_agent_tool_timeout | cave_network_error
- cave_request_failed
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/4b4ae45739663024.
Report an issue: GitHub.
Appendix: source
Thrown at packages/shared/binary-installer/installer.mjs:94
}
function timeoutMs() {
const raw = process.env.CAVE_SETUP_TIMEOUT ?? "300";
const seconds = Number(raw);
if (!Number.isInteger(seconds) || seconds <= 0) {
throw new Error(`CAVE_SETUP_TIMEOUT must be a positive integer (got ${JSON.stringify(raw)})`);
}
return seconds * 1000;
}
async function asset(url, timeout) {
let response;
try {
response = await fetch(url, { signal: AbortSignal.timeout(timeout) });
} catch (error) {
throw new Error(`binary download failed: ${error.message}`);
}
if (!response.ok) throw new Error(`binary download failed: HTTP ${response.status}`);
return response;
}
function signedDigest(checksums, bundleRaw) {
try {
const bundle = JSON.parse(bundleRaw);
if (bundle.mediaType !== "application/vnd.dev.sigstore.bundle.v0.3+json") return false;
if (bundle.messageSignature?.messageDigest?.algorithm !== "SHA2_256") return false;
const digest = createHash("sha256").update(checksums).digest();
const bundled = Buffer.from(bundle.messageSignature.messageDigest.digest, "base64");
if (digest.length !== bundled.length || !digest.equals(bundled)) return false;
return verify(
"sha256",
Buffer.from(checksums),
createPublicKey(BINARY_SIGNING_PUBKEY),
Buffer.from(bundle.messageSignature.signature, "base64"),
);
} catch {View on GitHub (pinned to 3ee70a1026)