[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

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)


Diff Delta:
151
About 178 Diff Delta/hour
Classified as:  Bug Fix

hoxyq's Description of Work #37624 [DevTools Bug]: standalone startServer ignores the host argument and listens beyond loopback

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.

6 total changed files
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
Loading changes...
That’s the last hunk
Please mind the merge button