{"record":{"id":"a9a3422d9af3089e","repo":"lobehub/lobehub","slug":"too-many-redirects-while-downloading-binary","errorCode":null,"errorMessage":"Too many redirects while downloading binary","messagePattern":"Too many redirects while downloading binary","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/src/main/core/infrastructure/BinaryManager.ts","lineNumber":727,"sourceCode":"      }\n    },\n    manage,\n    name,\n    priority,\n  };\n}\n\n// ========================================\n// Internal helpers — download + Gatekeeper handling\n// ========================================\n\n/**\n * Follow HTTP(S) redirects and stream the body to `dest`. Mirrors the older\n * `scripts/download-agent-browser.mjs` semantics — 5 hops max, errors on\n * non-2xx, no checksum verification (left to the spec when needed).\n */\nasync function downloadWithRedirects(url: string, dest: string, maxRedirects = 5): Promise<void> {\n  if (maxRedirects <= 0) throw new Error('Too many redirects while downloading binary');\n\n  await new Promise<void>((resolve, reject) => {\n    https\n      .get(url, { headers: { 'User-Agent': 'lobehub-desktop-binary-manager' } }, (res) => {\n        if (\n          res.statusCode &&\n          res.statusCode >= 300 &&\n          res.statusCode < 400 &&\n          res.headers.location\n        ) {\n          const next = res.headers.location;\n          res.resume();\n          downloadWithRedirects(next, dest, maxRedirects - 1).then(resolve, reject);\n          return;\n        }\n\n        if (res.statusCode !== 200) {\n          res.resume();","sourceCodeStart":709,"sourceCodeEnd":745,"githubUrl":"https://github.com/lobehub/lobehub/blob/10f24d7ade75139093a9373b364f6bc91f3cd7db/apps/desktop/src/main/core/infrastructure/BinaryManager.ts#L709-L745","documentation":"Recursion guard inside downloadWithRedirects — decrements maxRedirects on each hop and throws when it reaches zero. The default cap is 5, mirroring the legacy scripts/download-agent-browser.mjs semantics. The error means the download URL entered a redirect loop or a chain longer than the cap; no body is written to dest.","triggerScenarios":"A release URL that bounces between CDN nodes (e.g. GitHub releases → objects.githubusercontent.com → release-assets → ...); a misconfigured mirror that returns 302 to itself; an OAuth-gated URL that bounces through a login redirect; HTTPS-to-HTTP downgrade chains that re-upgrade.","commonSituations":"GitHub release asset behind multiple CDN hops on a slow region; an enterprise proxy that injects extra 302s for content inspection; a stale release() function in the BinarySpec returning the API URL instead of the direct asset URL; a tag rename that left the old URL redirecting forever.","solutions":["Open the release URL in a browser with devtools open and count the 3xx hops — if it stabilises within 5, the issue is intermittent; if not, the URL is wrong.","Update the BinarySpec.manage.release function to return the final asset URL (e.g. the browser_download_url from the GitHub API) instead of the redirecting short link.","Raise the maxRedirects default in downloadWithRedirects if the CDN chain is legitimate and stable.","Pin a different pinnedVersion whose asset URL is a direct download.","If a corporate proxy is rewriting URLs, configure NO_PROXY/binaryManager to bypass it for the download host."],"exampleFix":"// before\nasync function downloadWithRedirects(url: string, dest: string, maxRedirects = 5): Promise<void> {\n  if (maxRedirects <= 0) throw new Error('Too many redirects while downloading binary');\n  // ...\n}\n\n// after — resolve the final URL via HEAD preflight and surface the hop count when exceeded\nasync function resolveFinalUrl(startUrl: string, maxHops = 10): Promise<string> {\n  let url = startUrl;\n  for (let i = 0; i < maxHops; i++) {\n    const res = await fetch(url, { method: 'HEAD', redirect: 'manual' });\n    if (res.status >= 300 && res.status < 400 && res.headers.get('location')) {\n      url = new URL(res.headers.get('location')!, url).toString();\n      continue;\n    }\n    return url;\n  }\n  throw new Error(`Too many redirects while downloading binary (>${maxHops} hops)`);\n}\nasync function downloadWithRedirects(url: string, dest: string): Promise<void> {\n  const finalUrl = await resolveFinalUrl(url);\n  // ...stream finalUrl to dest...\n}","handlingStrategy":"retry","validationCode":"import { fetch } from 'undici';\n\nasync function assertDownloadable(url: string, maxHops = 5): Promise<void> {\n  let u = url;\n  for (let i = 0; i < maxHops; i++) {\n    const res = await fetch(u, { method: 'HEAD', redirect: 'manual' });\n    if (res.status >= 300 && res.status < 400 && res.headers.get('location')) {\n      u = new URL(res.headers.get('location')!, u).toString();\n      continue;\n    }\n    if (res.status === 200) return;\n    throw new Error(`Pre-flight: ${url} returned HTTP ${res.status}`);\n  }\n  throw new Error(`Pre-flight: ${url} exceeded ${maxHops} redirects`);\n}","typeGuard":"function isRedirectLoopError(e: unknown): boolean {\n  return e instanceof Error && /Too many redirects/.test(e.message);\n}","tryCatchPattern":"try {\n  await binaryManager.install(name);\n} catch (e) {\n  if (e instanceof Error && /Too many redirects/.test(e.message)) {\n    // pin a different version whose asset URL is a direct download\n    await binaryManager.install(name, fallbackPinnedVersion);\n  } else throw e;\n}","preventionTips":["Pre-flight HEAD the release URL to detect redirect loops before streaming.","Pin BinarySpec.manage.release to return the final asset URL (e.g. browser_download_url) instead of a short link.","Bypass corporate proxies for the download host via NO_PROXY if they inject extra redirects.","Bump maxRedirects in downloadWithRedirects if your CDN chain is legitimately longer than 5."],"tags":["binary-manager","download","redirect","http","infrastructure"],"backgroundTag":null,"analyzedSha":"10f24d7ade75139093a9373b364f6bc91f3cd7db","analyzedAt":"2026-08-12T11:43:19.543Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}