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

  1. Print the refused hostname from the message and check whether it should be added to RELEASE_HOSTS.
  2. Confirm the release URL resolves within the official hosts (e.g. github.com / objects.githubusercontent.com).
  3. Check for proxies or security appliances rewriting the redirect chain and bypass them.
  4. 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

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


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) <= 0

View on GitHub (pinned to 433685b202)