expo/expo · error · Error
Failed to download bundle: ${bundleRes.status}
Error message
Failed to download bundle: ${bundleRes.status} What it means
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.
Source
Thrown at apps/expo-go/scripts/download-snack-runtime.js:325
// Ensure output directories exist
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
fs.mkdirSync(ASSETS_DIR, { recursive: true });
// Download the bundle
console.log(`\nDownloading JS bundle...`);
const bundlePath = path.join(OUTPUT_DIR, BUNDLE_NAME);
let bundleSize;
if (fs.existsSync(bundlePath)) {
bundleSize = fs.statSync(bundlePath).size;
console.log(` Skipping ${BUNDLE_NAME} (already exists, ${formatSize(bundleSize)})`);
} else {
const bundleHeaders = authHeaders?.[launchAsset.key] || {};
const bundleRes = await fetch(launchAsset.url, { headers: bundleHeaders });
if (bundleRes.status !== 200) {
throw new Error(`Failed to download bundle: ${bundleRes.status}`);
}
fs.writeFileSync(bundlePath, bundleRes.body);
bundleSize = bundleRes.body.length;
console.log(` Downloaded ${BUNDLE_NAME} (${formatSize(bundleSize)})`);
}
// Download all assets
console.log(`\nDownloading ${assets.length} assets...`);
let totalAssetsSize = 0;
let downloadedCount = 0;
let skippedCount = 0;
for (let i = 0; i < assets.length; i++) {
const result = await downloadAsset(assets[i], authHeaders, i, assets.length);
totalAssetsSize += result.size;
if (result.skipped) {View on GitHub (pinned to b09195aac2)
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.
Example fix
// before
const bundleRes = await fetch(launchAsset.url, { headers: bundleHeaders });
if (bundleRes.status !== 200) {
throw new Error(`Failed to download bundle: ${bundleRes.status}`);
}
// after — retry with backoff for transient failures
async function fetchWithRetry(url, headers, retries = 3) {
for (let i = 0; i < retries; i++) {
const res = await fetch(url, { headers });
if (res.status === 200) return res;
if (res.status >= 500 || res.status === 429) {
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
continue;
}
throw new Error(`Failed to download bundle: ${res.status}`);
}
throw new Error('Failed to download bundle after retries');
}
const bundleRes = await fetchWithRetry(launchAsset.url, bundleHeaders); Defensive patterns
Strategy: retry
Validate before calling
// Check that the bundle URL is reachable before writing to disk
async function checkBundleUrl(url, headers) {
const res = await fetch(url, { headers, method: 'HEAD' });
return res.status === 200;
} Try / catch
const bundleRes = await fetch(launchAsset.url, { headers: bundleHeaders });
if (bundleRes.status !== 200) {
if (bundleRes.status === 403 || bundleRes.status === 404) {
throw new Error(`Bundle URL invalid/expired (${bundleRes.status}). Re-run to get a fresh manifest.`);
}
// Retry transient failures
for (let i = 0; i < 3; i++) {
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
const retry = await fetch(launchAsset.url, { headers: bundleHeaders });
if (retry.status === 200) { /* success */ break; }
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Invalid manifest: missing launchAsset
- package.json not found at ${runtimePkgPath}
- bundledNativeModules.json not found at ${BUNDLED_NATIVE_MODU
- API
- Custom e2e flows kept failing after ${maxAttempts} attempts:
AI-assisted analysis of expo/expo@b09195aac2 (2026-08-12).
Data as JSON: /api/errors/a06a30fa155266b7.
Report an issue: GitHub.