Skip to content

Fix stale packager connections on Android - #58971

Draft
vonovak wants to merge 1 commit into
react:mainfrom
vonovak:codex/stale-packager-connection-test
Draft

vonovak wants to merge 1 commit into
react:mainfrom
vonovak:codex/stale-packager-connection-test

Conversation

@vonovak

@vonovak vonovak commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Summary:

On Android, with two Metro servers running, an app that switches from one to the other (via Change Bundle Location, setBundleSource, or a dev client) can keep its packager connection to the first. Pressing r in the first Metro's terminal then still reloads the app. I hit this while working in several worktrees at once, with several emulators, killing and restarting Metro servers. There it depends on timing. Change Bundle Location in the dev menu should reproduce it every time, because it never closed the old connection.

DevServerHelper ran open and close in separate AsyncTasks, so a close could overtake an open, and repeated opens could leak a client. Change Bundle Location never closed the old connection. ReconnectingWebSocket also kept a socket whose handshake finished after closeQuietly().

Changes:

  • Open and close run in order on one single-threaded executor. An open for a new host replaces the old client.
  • ReconnectingWebSocket closes a socket that opens after closeQuietly().
  • Change Bundle Location closes the connection before reloading, and rejects a host that would make the connection throw.

Notes: open and close are now asynchronous, and customCommandHandlers() handlers must not block.

Changelog:

[ANDROID] [FIXED] - Fix the app staying connected to the previous Metro server after switching to another one

Test Plan:

New tests in DevServerHelperTest, ReconnectingWebSocketTest and DevSupportHttpClientTest, plus the existing JSPackagerClientTest, all pass.

DevServerHelper opened and closed the packager connection in separate
AsyncTasks on THREAD_POOL_EXECUTOR. A close could run before a pending
open, and repeated opens could create two clients. Changing the bundle
location from the dev menu also kept the old connection. In both cases,
the old dev server could still reload the app.

- Run openPackagerConnection and closePackagerConnection as tasks on one
  single-threaded executor. Tasks run in call order, off the UI thread,
  and only that thread uses packagerClient. The executor names its
  thread ReactPackagerConnection and stops it after 30 seconds without
  work.
- Remember the host of the open client. If an open finds a client for a
  different host, close it and connect to the new host.
- Check clientId on the caller's thread. A failed init() still crashes
  the app, as before; packagerClient stays null, so a later open can
  retry.
- Change Bundle Location: close the packager connection before the
  reload, as setBundleSource does. Trim the input, then reject a host
  that OkHttp cannot parse in the connection URL and show a toast.
  Before, an invalid host was saved without a check.
- ReconnectingWebSocket: change close state under the socket's monitor,
  so no message handler starts after closeQuietly() returns. Close a
  socket whose handshake finishes after closeQuietly() instead of
  reporting it as connected.
- Add DevServerHelperTest, ReconnectingWebSocketTest and
  DevSupportHttpClientTest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 9, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Expo Partner: Expo Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant