{"record":{"id":"a06a30fa155266b7","repo":"expo/expo","slug":"failed-to-download-bundle-bundleres-status","errorCode":null,"errorMessage":"Failed to download bundle: ${bundleRes.status}","messagePattern":"Failed to download bundle: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/expo-go/scripts/download-snack-runtime.js","lineNumber":325,"sourceCode":"\n  // Ensure output directories exist\n  fs.mkdirSync(OUTPUT_DIR, { recursive: true });\n  fs.mkdirSync(ASSETS_DIR, { recursive: true });\n\n  // Download the bundle\n  console.log(`\\nDownloading JS bundle...`);\n  const bundlePath = path.join(OUTPUT_DIR, BUNDLE_NAME);\n  let bundleSize;\n\n  if (fs.existsSync(bundlePath)) {\n    bundleSize = fs.statSync(bundlePath).size;\n    console.log(`  Skipping ${BUNDLE_NAME} (already exists, ${formatSize(bundleSize)})`);\n  } else {\n    const bundleHeaders = authHeaders?.[launchAsset.key] || {};\n    const bundleRes = await fetch(launchAsset.url, { headers: bundleHeaders });\n\n    if (bundleRes.status !== 200) {\n      throw new Error(`Failed to download bundle: ${bundleRes.status}`);\n    }\n\n    fs.writeFileSync(bundlePath, bundleRes.body);\n    bundleSize = bundleRes.body.length;\n    console.log(`  Downloaded ${BUNDLE_NAME} (${formatSize(bundleSize)})`);\n  }\n\n  // Download all assets\n  console.log(`\\nDownloading ${assets.length} assets...`);\n\n  let totalAssetsSize = 0;\n  let downloadedCount = 0;\n  let skippedCount = 0;\n\n  for (let i = 0; i < assets.length; i++) {\n    const result = await downloadAsset(assets[i], authHeaders, i, assets.length);\n    totalAssetsSize += result.size;\n    if (result.skipped) {","sourceCodeStart":307,"sourceCodeEnd":343,"githubUrl":"https://github.com/expo/expo/blob/b09195aac27c9e4a63daaf12dcb6f00b6b2e08ee/apps/expo-go/scripts/download-snack-runtime.js#L307-L343","documentation":"Thrown when the HTTP download of the JS bundle (launchAsset.url from the manifest) returns a non-200 status. The bundle is the Hermes bytecode (.hbc) file that Expo Go embeds as the Snack runtime. A non-200 means the CDN URL was unreachable, expired, or the auth headers were rejected.","triggerScenarios":"After parsing the manifest successfully, fetch(launchAsset.url, { headers: bundleHeaders }) returns a status code other than 200. bundleHeaders come from extensions.assetRequestHeaders[launchAsset.key], which may be undefined if no auth is required.","commonSituations":"The signed CDN URL in the manifest has expired (EAS Updates URLs are time-limited); the asset requires authentication headers that weren't included; transient CDN outage or rate limiting (429/503); the bundle file was deleted from storage after the manifest was published.","solutions":["Re-run the script — EAS Updates signs fresh CDN URLs on each manifest request, so an expired URL self-heals.","If 403: verify the launchAsset.key is present in extensions.assetRequestHeaders and the headers are being applied (check authHeaders?.[launchAsset.key]).","If 404: the published update's bundle was likely deleted from storage; republish the update.","If 429/503: transient CDN issue, retry with backoff."],"exampleFix":"// before\nconst bundleRes = await fetch(launchAsset.url, { headers: bundleHeaders });\nif (bundleRes.status !== 200) {\n  throw new Error(`Failed to download bundle: ${bundleRes.status}`);\n}\n\n// after — retry with backoff for transient failures\nasync function fetchWithRetry(url, headers, retries = 3) {\n  for (let i = 0; i < retries; i++) {\n    const res = await fetch(url, { headers });\n    if (res.status === 200) return res;\n    if (res.status >= 500 || res.status === 429) {\n      await new Promise(r => setTimeout(r, 1000 * (i + 1)));\n      continue;\n    }\n    throw new Error(`Failed to download bundle: ${res.status}`);\n  }\n  throw new Error('Failed to download bundle after retries');\n}\nconst bundleRes = await fetchWithRetry(launchAsset.url, bundleHeaders);","handlingStrategy":"retry","validationCode":"// Check that the bundle URL is reachable before writing to disk\nasync function checkBundleUrl(url, headers) {\n  const res = await fetch(url, { headers, method: 'HEAD' });\n  return res.status === 200;\n}","typeGuard":null,"tryCatchPattern":"const bundleRes = await fetch(launchAsset.url, { headers: bundleHeaders });\nif (bundleRes.status !== 200) {\n  if (bundleRes.status === 403 || bundleRes.status === 404) {\n    throw new Error(`Bundle URL invalid/expired (${bundleRes.status}). Re-run to get a fresh manifest.`);\n  }\n  // Retry transient failures\n  for (let i = 0; i < 3; i++) {\n    await new Promise(r => setTimeout(r, 1000 * (i + 1)));\n    const retry = await fetch(launchAsset.url, { headers: bundleHeaders });\n    if (retry.status === 200) { /* success */ break; }\n  }\n}","preventionTips":["Re-run the script if the CDN URL has expired — EAS signs fresh URLs on each manifest request.","Remove the existing bundle file to force a re-download if the manifest has a new URL.","Check that authHeaders are correctly extracted from extensions.assetRequestHeaders."],"tags":["network","cdn","eas-updates","expo-go","snack-runtime","retry"],"backgroundTag":null,"analyzedSha":"b09195aac27c9e4a63daaf12dcb6f00b6b2e08ee","analyzedAt":"2026-08-12T15:59:58.304Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}