fix(android): actually download remote http(s) jsBundleSource#37
fix(android): actually download remote http(s) jsBundleSource#37akelmanson wants to merge 3 commits into
Conversation
Loading a sandbox from a remote URL never worked on Android: - `createBundleLoader` mapped an http(s) source to `JSBundleLoader.createFileLoader(url)`, but that loader calls `loadScriptFromFile`, which treats its argument as a local path — a remote URL is never downloaded. - Even with a working loader, the sandbox ReactHost was created with developer support enabled, so in dev builds the runtime ignored the loader and fetched the bundle from Metro using `jsMainModulePath`, turning `http://host/x.bundle` into `http://localhost:8081/http://host/x.bundle.bundle` (a 404). Fix: - Prefetch the remote bundle to a cache file off the main thread (avoids NetworkOnMainThreadException) and load it via `createCachedBundleFromNetworkLoader`, preserving the source URL for stack traces. Returns null (existing failure path) on download error. - Disable developer support for remote (http/https) sources only, so the runtime uses our loader instead of Metro. Local asset/name sources keep dev support and Fast Refresh. Verified on a device: host loads the sandbox bundle from a remote HTTP URL in a debug build, renders, and exchanges messages both ways. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The remote (http/https) loader downloaded the bundle on every sandbox VM creation, so each cold start re-fetched the full bundle and failed when offline. Treat a remote URL as immutable: if the cached file for that exact URL already exists, reuse it and skip the network. Repeated loads (cold starts) become instant and work offline; a partial/failed download is deleted so a later load retries cleanly. To publish an update, change the URL (version segment or `?v=` query) — a new URL misses the cache and is fetched once. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
createCachedBundleFromNetworkLoader loads the cached file with
loadSynchronously=false. On RN 0.85 bridgeless this executes the bundle
before the TurboModule JSI bindings are installed on the runtime, so the
bundle's InitializeCore hits TurboModuleRegistry.getEnforcing('PlatformConstants')
with no proxy registered and the sandbox VM aborts. Because this happens
before the sandbox error handler is installed, it escalates to a native
fatal and takes the host process down with it.
Load the already-downloaded cache file synchronously via
JSBundleLoader.createFileLoader(path, sourceURL, true) — mirroring how the
built-in asset loader runs the main bundle — so the runtime is ready before
the bundle runs. The original URL is kept as sourceURL for stack traces.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RN 0.85 compatibility + a design question on
|
| value | Android | iOS (bundleURL) |
|---|---|---|
https://… |
downloads → loads file | returns URL (native loader fetches) |
file:///abs/path |
falls through to assets:// ❌ |
url.scheme == "file" → loads ✅ |
/abs/path |
falls through to assets:// ❌ |
no scheme → resource lookup ❌ |
"name" |
assets://name |
RCTBundleURLProvider / name.jsbundle |
(iOS rows are from reading bundleURL — I couldn't test iOS myself.)
Options for a local-file / file:// capability
- A —
file://only (minimal, portable). Add a branch that loadsfile://as a local file: AndroidcreateFileLoader(path, src, true); iOS already handles it viaurl.scheme. One canonical form, works on both. Document that host-downloaded bundles are passed asfile://. - B —
file://+ bare absolute paths (lenient). Same as A, plus accept a leading/(Android trivially; iOS needs a[NSURL fileURLWithPath:]branch). Matches whatreact-native-fsreturns (bare paths); a bit more surface. - C — host-owns-fetch (network-free lib). Treat the in-lib
http(s)download as optional/deprecated and makefile:///asset the primary path; the host downloads + verifies and passesfile://. Cleanest security boundary, but the biggest reframing of this PR.
I have a working Android branch for A/B locally and am happy to push whichever direction you prefer. Note the loadSynchronously = true caveat applies to any FS-loaded bundle on 0.85. Thoughts?
Summary
Loading a sandbox from a remote URL (
jsBundleSource="https://…/x.bundle") does not work on Android today. Two issues compound:createBundleLoadermaps anhttp(s)source toJSBundleLoader.createFileLoader(url), but that loader callsloadScriptFromFile, which treats its argument as a local file path. A remote URL never resolves.ReactHostImplis created with developer support enabled, so in a debug build the runtime ignoresjsBundleLoaderand fetches the bundle from the Metro dev server usingjsMainModulePath. The URL gets mangled intohttp://localhost:8081/http://host/x.bundle.bundle→ 404 → the sandbox ReactInstance fails to initialize.Repro before this PR (debug build):
Fix
createBundleLoader, forhttp(s)sources: prefetch the bundle to a cache file off the main thread (a direct fetch would throwNetworkOnMainThreadException), then load it viaJSBundleLoader.createCachedBundleFromNetworkLoader(url, cacheFile), which keeps the original source URL for stack traces. On any download error it returnsnull, reusing the existing failure path.ReactHostImplwith developer support disabled for remote sources only. Local asset/name sources ("index","sandbox", …) keep dev support and Fast Refresh unchanged.Testing
Verified end-to-end on a device with a debug host build:
SandboxRNDelegate: Downloaded remote bundle 'http://…/sandbox.bundle' (978066 bytes)ReactNativeJS: Running "SandboxApp"inc, sandbox → hostcount).Notes
🤖 Generated with Claude Code