Hmbown/CodeWhale · error
Release redirect refused
Error message
Release redirect refused: ${target.hostname} What it means
fetchReleaseWeb follows at most 3 redirects and only to https URLs whose hostname is in the RELEASE_HOSTS allowlist. Any redirect exceeding those limits is refused with this error to prevent open-redirect or downgrade attacks during release downloads.
Solutions
- Print the refused hostname from the message and check whether it should be added to RELEASE_HOSTS.
- Confirm the release URL resolves within the official hosts (e.g. github.com / objects.githubusercontent.com).
- Check for proxies or security appliances rewriting the redirect chain and bypass them.
- If a new legitimate host is needed, add it to the RELEASE_HOSTS allowlist and redeploy.
Example fix
// before
const res = await fetchReleaseWeb('https://mirror.example.com/app.dmg');
// after
const res = await fetchReleaseWeb('https://github.com/org/repo/releases/latest/download/app.dmg'); Defensive patterns
Strategy: try-catch
Validate before calling
const u = new URL(releaseUrl);
if (u.protocol !== 'https:' || !['github.com','objects.githubusercontent.com'].includes(u.hostname)) throw new Error('untrusted release host'); Try / catch
try { await fetchReleaseWeb(url) } catch (e) { if (/Release redirect refused/.test(e.message)) log.warn('off-allowlist host:', e.message); throw e; } Prevention
- Only download from official release hosts.
- Inspect redirect chains (curl -sIL) when adding new mirrors.
- Never add unvetted hosts to RELEASE_HOSTS.
When it happens
Trigger: A release URL redirect chain exceeds 3 hops, redirects to http:, or redirects to a hostname not in RELEASE_HOSTS.
Common situations: Release asset moved behind a third-party CDN; a mirror or proxy host redirecting off-domain; a redirect loop between two hosts; institutional proxy rewriting https to http.
Related errors
- bundle redirects may not change URL scheme
- The update download redirected to an unexpected host.
- Codewhale web is loopback-only and must bind to 127.0.0.1
- failed to resolve latest stable release from
- MCP HTTP destination is a restricted IP address
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/0dfc56be66af212c.
Report an issue: GitHub.
Appendix: source
Thrown at web/lib/computer-use-release.ts:120
text += decoder.decode(value, { stream: true });
}
return JSON.parse(text + decoder.decode());
} finally { await reader.cancel(); reader.releaseLock(); }
}
const WEB_HEADERS = { "User-Agent": "codewhale-web" };
/** GET a release web endpoint, following at most three 302s and only onto GitHub's release hosts over https. */
async function fetchReleaseWeb(url: string): Promise<Response> {
for (let hops = 0; ; hops++) {
const response = await fetch(url, { redirect: "manual", headers: WEB_HEADERS, signal: AbortSignal.timeout(5000) });
if (![301, 302, 307, 308].includes(response.status)) return response;
await response.body?.cancel();
const location = response.headers.get("location");
if (!location) throw new Error("Release redirect without a location");
const target = new URL(location, url);
if (hops >= 3 || target.protocol !== "https:" || !RELEASE_HOSTS.has(target.hostname)) {
throw new Error(`Release redirect refused: ${target.hostname}`);
}
url = target.href;
}
}
/** Resolve the download without the GitHub API: read the latest receipt from the
* release web endpoint, then confirm GitHub serves the archive the receipt names. */
async function receiptQualifiedRelease(): Promise<ComputerUseRelease> {
try {
const response = await fetchReleaseWeb(`${COMPUTER_USE_REPO}/releases/latest/download/release.json`);
if (response.status === 404) return { status: "pending" };
if (!response.ok) return { status: "unavailable" };
const receipt = record(await boundedJson(response, 16 * 1024));
const version = typeof receipt.version === "string" && /^\d+\.\d+\.\d+$/.test(receipt.version) ? receipt.version : null;
const archive = `Codewhale-Computer-Use-${version}-macos-universal.zip`;
if (!version || receipt.platform !== "macos" || receipt.arch !== "universal" || receipt.notarized !== true
|| receipt.archive !== archive || typeof receipt.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(receipt.sha256)
|| !Number.isSafeInteger(receipt.size) || (receipt.size as number) <= 0View on GitHub (pinned to 433685b202)