{"record":{"id":"afb492aa20f4b1f8","repo":"Hmbown/CodeWhale","slug":"release-redirect-without-a-location","errorCode":null,"errorMessage":"Release redirect without a location","messagePattern":"Release redirect without a location","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web/lib/computer-use-release.ts","lineNumber":117,"sourceCode":"      if (done) break;\n      length += value.byteLength;\n      if (length > limit) throw new Error(\"Release response exceeds size limit\");\n      text += decoder.decode(value, { stream: true });\n    }\n    return JSON.parse(text + decoder.decode());\n  } finally { await reader.cancel(); reader.releaseLock(); }\n}\n\nconst WEB_HEADERS = { \"User-Agent\": \"codewhale-web\" };\n\n/** GET a release web endpoint, following at most three 302s and only onto GitHub's release hosts over https. */\nasync function fetchReleaseWeb(url: string): Promise<Response> {\n  for (let hops = 0; ; hops++) {\n    const response = await fetch(url, { redirect: \"manual\", headers: WEB_HEADERS, signal: AbortSignal.timeout(5000) });\n    if (![301, 302, 307, 308].includes(response.status)) return response;\n    await response.body?.cancel();\n    const location = response.headers.get(\"location\");\n    if (!location) throw new Error(\"Release redirect without a location\");\n    const target = new URL(location, url);\n    if (hops >= 3 || target.protocol !== \"https:\" || !RELEASE_HOSTS.has(target.hostname)) {\n      throw new Error(`Release redirect refused: ${target.hostname}`);\n    }\n    url = target.href;\n  }\n}\n\n/** Resolve the download without the GitHub API: read the latest receipt from the\n * release web endpoint, then confirm GitHub serves the archive the receipt names. */\nasync function receiptQualifiedRelease(): Promise<ComputerUseRelease> {\n  try {\n    const response = await fetchReleaseWeb(`${COMPUTER_USE_REPO}/releases/latest/download/release.json`);\n    if (response.status === 404) return { status: \"pending\" };\n    if (!response.ok) return { status: \"unavailable\" };\n    const receipt = record(await boundedJson(response, 16 * 1024));\n    const version = typeof receipt.version === \"string\" && /^\\d+\\.\\d+\\.\\d+$/.test(receipt.version) ? receipt.version : null;\n    const archive = `Codewhale-Computer-Use-${version}-macos-universal.zip`;","sourceCodeStart":99,"sourceCodeEnd":135,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/web/lib/computer-use-release.ts#L99-L135","documentation":"fetchReleaseWeb manually follows release-download redirects and only accepts hops landing on a whitelisted RELEASE_HOSTS https host. When a 3xx response carries no Location header there is nothing to follow, so the fetcher aborts rather than looping or returning a redirect response. It guards against malformed servers and redirect-based attacks.","triggerScenarios":"A request to a release URL returns status 301/302/307/308 but response.headers.get('location') is null.","commonSituations":"Misconfigured CDN/proxy in front of the release host emitting bare 3xx; captive portals returning redirects without Location; GitHub returning an unusual redirect shape during incidents; an intercepted HTTPS proxy stripping headers.","solutions":["Inspect the raw response with curl -sI <url> to confirm the redirect lacks a Location header and fix the server/proxy.","Verify the release URL points at the expected host (GitHub releases), not a custom mirror emitting bad redirects.","Disable any intercepting proxy/VPN or add its host to RELEASE_HOSTS if it legitimately serves releases.","Retry later if GitHub is having an incident; otherwise fall back to the GitHub API resolution path."],"exampleFix":"// before\nconst res = await fetchReleaseWeb(releaseUrl);\n// after\nlet res;\ntry { res = await fetchReleaseWeb(releaseUrl); }\ncatch (e) { if (String(e.message).includes('without a location')) res = await fetchReleaseViaApi(); else throw e; }","handlingStrategy":"fallback","validationCode":"const res = await fetch(url, { method: 'HEAD', redirect: 'manual' });\nif ([301,302,307,308].includes(res.status) && !res.headers.get('location')) console.warn('redirect without location; use API fallback');","typeGuard":null,"tryCatchPattern":"try { download() } catch (e) { if (String(e.message).includes('Release redirect')) return fetchViaGitHubApi(); throw e; }","preventionTips":["Probe release URLs with a HEAD request before scripted downloads.","Avoid routing release downloads through proxies/CDNs that mangle redirects.","Keep a non-redirect fallback resolution path (GitHub API) wired in."],"tags":["network","redirect","http"],"backgroundTag":"unexpected-response-shape","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}