Skip to content

Commit 738b7ae

Browse files
authored
fix(android): download remote http(s) jsBundleSource correctly
Prefetch remote bundles to a cache file on a worker thread instead of passing the URL to createFileLoader (which treats it as a local path). Disable dev support for remote sources so Metro doesn't intercept the load. Synchronous load required on RN 0.85 bridgeless.
1 parent b6ccdc6 commit 738b7ae

1 file changed

Lines changed: 58 additions & 3 deletions

File tree

packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeDelegate.kt

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,20 @@ import com.facebook.react.runtime.ReactHostImpl
2727
import com.facebook.react.runtime.hermes.HermesInstance
2828
import com.facebook.react.shell.MainReactPackage
2929
import com.facebook.react.uimanager.ViewManager
30+
import java.io.File
31+
import java.net.HttpURLConnection
32+
import java.net.URL
3033

3134
class SandboxReactNativeDelegate(
3235
private val context: Context,
3336
) {
3437
companion object {
3538
private const val TAG = "SandboxRNDelegate"
3639

40+
private const val REMOTE_BUNDLE_CONNECT_TIMEOUT_MS = 10_000
41+
private const val REMOTE_BUNDLE_READ_TIMEOUT_MS = 15_000
42+
private const val REMOTE_BUNDLE_DOWNLOAD_TIMEOUT_MS = 20_000L
43+
3744
private val sharedHosts = mutableMapOf<String, SharedReactHost>()
3845
private val registeredSubstitutionPackages = mutableListOf<ReactPackage>()
3946
private val registeredHostPackages = mutableListOf<ReactPackage>()
@@ -200,13 +207,21 @@ class SandboxReactNativeDelegate(
200207
val componentFactory = ComponentFactory()
201208
DefaultComponentsRegistry.register(componentFactory)
202209

210+
// For a remote (http/https) bundle, disable developer support on this
211+
// ReactHost. With dev support enabled the runtime ignores jsBundleLoader
212+
// and fetches the bundle from the Metro dev server using jsMainModulePath,
213+
// turning the URL into http://localhost:8081/<url>.bundle (a 404). Local
214+
// sources ("index"/asset names) keep dev support so Fast Refresh works.
215+
val isRemoteBundle =
216+
capturedBundleSource.startsWith("http://") ||
217+
capturedBundleSource.startsWith("https://")
203218
host =
204219
ReactHostImpl(
205220
sandboxContext,
206221
hostDelegate,
207222
componentFactory,
208-
true,
209-
true,
223+
!isRemoteBundle,
224+
!isRemoteBundle,
210225
)
211226

212227
ownsReactHost = true
@@ -293,7 +308,12 @@ class SandboxReactNativeDelegate(
293308
if (bundleSource.isEmpty()) return null
294309
return when {
295310
bundleSource.startsWith("http://") || bundleSource.startsWith("https://") -> {
296-
JSBundleLoader.createFileLoader(bundleSource)
311+
// createFileLoader(url) treats its argument as a local path and never
312+
// downloads. Prefetch to a cache file on a worker thread instead.
313+
val cacheFile = downloadRemoteBundle(bundleSource) ?: return null
314+
// Load synchronously: on RN 0.85 bridgeless an async load executes the
315+
// bundle before TurboModule JSI bindings are installed, causing a fatal.
316+
JSBundleLoader.createFileLoader(cacheFile.absolutePath, bundleSource, true)
297317
}
298318

299319
else -> {
@@ -302,6 +322,41 @@ class SandboxReactNativeDelegate(
302322
}
303323
}
304324

325+
private fun downloadRemoteBundle(bundleSource: String): File? {
326+
val cacheFile = File(context.cacheDir, "sandbox-remote-${bundleSource.hashCode()}.bundle")
327+
if (cacheFile.exists() && cacheFile.length() > 0L) {
328+
Log.d(TAG, "Reusing cached bundle '$bundleSource' (${cacheFile.length()} bytes)")
329+
return cacheFile
330+
}
331+
var downloadError: Exception? = null
332+
val worker =
333+
Thread {
334+
try {
335+
val connection = URL(bundleSource).openConnection() as HttpURLConnection
336+
connection.connectTimeout = REMOTE_BUNDLE_CONNECT_TIMEOUT_MS
337+
connection.readTimeout = REMOTE_BUNDLE_READ_TIMEOUT_MS
338+
try {
339+
connection.inputStream.use { input ->
340+
cacheFile.outputStream().use { output -> input.copyTo(output) }
341+
}
342+
} finally {
343+
connection.disconnect()
344+
}
345+
} catch (e: Exception) {
346+
downloadError = e
347+
}
348+
}
349+
worker.start()
350+
worker.join(REMOTE_BUNDLE_DOWNLOAD_TIMEOUT_MS)
351+
if (downloadError != null || !cacheFile.exists() || cacheFile.length() == 0L) {
352+
cacheFile.delete()
353+
Log.e(TAG, "Failed to download remote bundle '$bundleSource'", downloadError)
354+
return null
355+
}
356+
Log.d(TAG, "Downloaded remote bundle '$bundleSource' (${cacheFile.length()} bytes)")
357+
return cacheFile
358+
}
359+
305360
fun onJSIBindingsInstalled(stateHandle: Long) {
306361
jsiStateHandle = stateHandle
307362
// Store on the shared host so views that reuse this host can

0 commit comments

Comments
 (0)