From 3ea96b0328b42816324832276cc9371507169202 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Tue, 15 Sep 2026 07:30:47 -0700 Subject: [PATCH] Improve diagnostics when the packager cannot be reached or used (#58519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Three gaps made an unusable packager connection close to impossible to diagnose from logs. `ReconnectingWebSocket` logged only `Couldn't connect to "", will silently retry`, discarding the `Throwable` that `onFailure` already receives. That throwable is the only thing separating a server which rejected the WebSocket upgrade from one that was never reached — okhttp reports the former as `Expected HTTP 101 response but was ' '` — so without it the two produced byte-identical output. It is now logged, and `onClosed` distinguishes a peer-side close by including its code and reason. `ReactHostImpl` silently falls back to the bundle shipped inside the app when `useDevSupport` is true but `allowPackagerServerAccess` is false. No packager request is issued in that state, so nothing downstream has anything to report, and the app appears to ignore a perfectly healthy development server for no visible reason. That combination now warns; the existing debug log is kept for the ordinary case where dev support is off too. Also repairs a comment in `ReactHostImpl` that had unrelated text spliced into the middle of a word. No public API changes, so `ReactAndroid.api` is unaffected. Changelog: [Android][Fixed] - Log the underlying cause when a packager connection fails [Android][Added] - Warn when dev support is enabled but packager server access is disabled Reviewed By: rubennorte Differential Revision: D119721144 --- .../packagerconnection/ReconnectingWebSocket.kt | 17 +++++++++++++---- .../com/facebook/react/runtime/ReactHostImpl.kt | 14 +++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.kt index 87d80827a336..ada82e7a8d30 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.kt @@ -59,11 +59,17 @@ public class ReconnectingWebSocket( } } - private fun reconnect() { + private fun reconnect(detail: String? = null, cause: Throwable? = null) { check(!closed) { "Can't reconnect closed client" } if (!suppressConnectionErrors) { - FLog.w(TAG, "Couldn't connect to \"$url\", will silently retry") + val suffix = if (detail == null) "" else " ($detail)" + val message = "Couldn't connect to \"$url\"$suffix, will silently retry" + if (cause == null) { + FLog.w(TAG, message) + } else { + FLog.w(TAG, message, cause) + } suppressConnectionErrors = true } @@ -107,7 +113,10 @@ public class ReconnectingWebSocket( } if (!closed) { connectionCallback?.onDisconnected() - reconnect() + // `t` distinguishes a rejected upgrade from never reaching the server: okhttp + // reports the former as `Expected HTTP 101 response but was ' '`, + // so `response` carries nothing extra worth reading off it here. + reconnect(cause = t) } } @@ -126,7 +135,7 @@ public class ReconnectingWebSocket( this.webSocket = null if (!closed) { connectionCallback?.onDisconnected() - reconnect() + reconnect("closed by peer with code $code: \"$reason\"") } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt index 4db89e7a3143..dfb71cdf370f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt @@ -1147,8 +1147,7 @@ public class ReactHostImpl( { task -> val isMetroRunning = checkNotNull(task.getResult()) if (isMetroRunning) { - // Since metro is running, fetcxception(method, "ReactContext is null. Reload - // reason: $h the JS bundle from the server + // Since metro is running, fetch the JS bundle from the server loadJSBundleFromMetro() } else { Task.forResult(reactHostDelegate.jsBundleLoader) @@ -1157,7 +1156,16 @@ public class ReactHostImpl( bgExecutor, ) } else { - if (ReactBuildConfig.DEBUG) { + if (useDevSupport) { + // Dev support is on, so the developer expects to be editing JS against a packager, but + // the bundle can only come from the app. Nothing downstream reports this, because no + // packager request is ever made. + FLog.w( + TAG, + "Dev support is enabled but packager server access is not. The JS bundle will be " + + "loaded from the app and the development server will not be used.", + ) + } else if (ReactBuildConfig.DEBUG) { FLog.d(TAG, "Packager server access is disabled in this environment") }