[DevTools] Bind standalone server to the configured host #37820

Open
hoxyq opened 6:17pm on October 12, 2026 wants to merge 151 Ξ” into facebook/react main from
devtools-standalone-listen-host

Pull Request Overview

  • Opened on October 12, 2026
  • Status Open
  • Commit count 1 with first commit October 12, 2026

Total Delta

151 Total Diff Delta

Open Days

Open 25 weekdays

Test Delta

0 Diff Delta in Test Files
Breakdown by Phase

How long has this pull request spent in each phase of its lifecycle?

Fraction of total time Business days Phase
N/A 21.0 days Issue creation to work beginning
 
0.0 days Authoring 1 commit before pull request opened for review
 
0.0 days Awaiting first review
 
24.0 days Revising work with 0 commits in response to 0 reviews that left 1 comment

Total time for pull request still awaiting merge (longer than repo's target): 24.0 business days

Author avatar

[DevTools] Bind standalone server to the configured host

Summary

Fixes #37624. startServer(port, host) never passed host to httpServer.listen(), so the standalone server always listened on all interfaces, even with HOST=127.0.0.1.



  • host is now passed to listen(). Without a host, the server still listens on all interfaces and clients are told to connect to localhost, as before. React Native on physical devices and the app's LAN-IP snippet rely on this.

  • The Electron app passes HOST only when it's set. Defaulting it to localhost would bind ::1 only on Node 17+, breaking IPv4 clients such as the Android emulator (10.0.2.2).

Behavior change: an explicit HOST/host now restricts where the server listens. To only change the host clients connect to, use REACT_DEVTOOLS_CLIENT_HOST/clientOptions.host. The READMEs are updated.

Known issue, to be fixed separately: if binding fails (for example, a HOST that doesn't resolve), the existing retry-on-error path schedules two retries per failure, so retries double every second.

How did you test this change?


  • New standalone-test.js checks the address the server listens on. Before this change it gets :: instead of 127.0.0.1.

  • Built the Electron app from this branch and ran it with HOST unset. It connected to fixtures/devtools/standalone via both localhost and the LAN-IP snippet. It also connected to a React Native app on the iOS Simulator via both localhost and the LAN IP.

  • With 127.0.0.1, connecting through the LAN IP is now refused; before this change it succeeded.

Website or app

React DevTools standalone package: https://github.com/react/react/tree/main/packages/react-devtools

Repro steps

[email protected] passes its configured host to react-devtools-core/standalone.startServer(port, host). The default host is documented as localhost, but the supplied value is not applied to the HTTP listening socket.

  1. Create an empty directory and install the current packages used by this reproduction:
   npm init -y
   npm install [email protected] [email protected]
  1. Save the following as bind-check.cjs:
   'use strict';

   const { JSDOM, VirtualConsole } = require('jsdom');
   const net = require('node:net');
   const os = require('node:os');

   const dom = new JSDOM('<!doctype html><body></body>', {
     pretendToBeVisual: true,
     url: 'http://localhost',
     virtualConsole: new VirtualConsole(),
   });
   dom.window.ResizeObserver = class {
     observe() {}
     unobserve() {}
     disconnect() {}
   };

   for (const key of [
     'window',
     'document',
     'navigator',
     'HTMLElement',
     'Element',
     'Node',
     'localStorage',
     'sessionStorage',
   ]) {
     global[key] = key === 'window'
       ? dom.window
       : key === 'document'
         ? dom.window.document
         : dom.window[key];
   }
   global.getComputedStyle = dom.window.getComputedStyle.bind(dom.window);
   global.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
   global.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);

   const nonLoopback = Object.values(os.networkInterfaces())
     .flat()
     .filter(Boolean)
     .find(entry => entry.family === 'IPv4' && !entry.internal);
   if (!nonLoopback) throw new Error('No non-loopback IPv4 address found');

   const DevTools = require('react-devtools-core/standalone').default;
   DevTools.setContentDOMNode(document.body);

   const port = 18098;
   const requestedHost = '127.0.0.1';
   const server = DevTools.startServer(port, requestedHost);

   function finish(code) {
     server.close();
     setTimeout(() => process.exit(code), 50);
   }

   setTimeout(() => {
     const socket = net.connect(port, nonLoopback.address);
     socket.on('connect', () => {
       console.log(JSON.stringify({
         requestedHost,
         connectedVia: nonLoopback.address,
       }));
       socket.destroy();
       finish(0);
     });
     socket.on('error', error => {
       console.error(error);
       finish(1);
     });
   }, 100);

   setTimeout(() => finish(2), 5000);
  1. Run:
   node bind-check.cjs
  1. Although startServer() was explicitly called with 127.0.0.1, the connection through the same machine's non-loopback address succeeds:
   {"requestedHost":"127.0.0.1","connectedVia":"<non-loopback IPv4>"}

The test makes only a local connection to the same machine.

Expected: passing 127.0.0.1 or localhost restricts the listening socket to that host, as described by the API and standalone documentation.

Actual: the implementation calls httpServer.listen(port) without the supplied host, so Node listens on an unspecified address.

A direct fix would be to forward the argument:

httpServer.listen(port, host, callback)

Environment used: macOS, Node.js 20.19.5.

How often does this bug happen?

Every time

DevTools package (automated)

No response

DevTools version (automated)

No response

Error message (automated)

No response

Error call stack (automated)


Error component stack (automated)


GitHub query string (automated)


Comments Threads Pending Resolution

Resolved Comment Threads

No resolved comments have been left on this PR.