@samitouri / QOS-React / commits / c0060cf2a6

[DevTools] Enable support for the React DevTools Client to connect to different host/port/path (#35886)

## Summary This enables routing the React Dev Tools through a remote server by being able to specify host, port, and path for the client to connect to. Basically allowing the React Dev Tools server to have the client connect elsewhere. This setups a `clientOptions` which can be set up through environment variables when starting the React Dev Tools server. This change shouldn't affect the traditional usage for React Dev Tools. EDIT: the additional change was moved to another PR ## How did you test this change? Run React DevTools with ``` $ REACT_DEVTOOLS_CLIENT_HOST=<MY_HOST> REACT_DEVTOOLS_CLIENT_PORT=443 REACT_DEVTOOLS_CLIENT_USE_HTTPS=true REACT_DEVTOOLS_PATH=/__react_devtools__/ yarn start ``` Confirm that my application connects to the local React Dev Tools server/instance/electron app through my remote server.

Mushaheed Kapadia committed Feb 24, 2026 at 10:36 UTC c0060cf2a695d719152c939cfc3cced8f7da3e52
6 files changed +139 -16
packages/react-devtools-core/README.md
+39 -3
@@ -54,6 +54,7 @@ Each filter object must include `type` and `isEnabled`. Some filters also requir
54 |------------------------|---------------|---------------------------------------------------------------------------------------------------------------------------|
55 | `host` | `"localhost"` | Socket connection to frontend should use this host. |
56 | `isAppActive` | | (Optional) function that returns true/false, telling DevTools when it's ready to connect to React. |
57 +| `path` | `""` | Path appended to the WebSocket URI (e.g. `"/__react_devtools__/"`). Useful when proxying through a reverse proxy on a subpath. A leading `/` is added automatically if missing. |
58 | `port` | `8097` | Socket connection to frontend should use this port. |
59 | `resolveRNStyle` | | (Optional) function that accepts a key (number) and returns a style (object); used by React Native. |
60 | `retryConnectionDelay` | `200` | Delay (ms) to wait between retrying a failed Websocket connection |
@@ -141,16 +142,51 @@ function onStatus(
142 }
143 ```
144
144 -#### `startServer(port?: number, host?: string, httpsOptions?: Object, loggerOptions?: Object)`
145 +#### `startServer(port?, host?, httpsOptions?, loggerOptions?, path?, clientOptions?)`
146 Start a socket server (used to communicate between backend and frontend) and renders the DevTools UI.
147
148 This method accepts the following parameters:
149 | Name | Default | Description |
150 |---|---|---|
150 -| `port` | `8097` | Socket connection to backend should use this port. |
151 -| `host` | `"localhost"` | Socket connection to backend should use this host. |
151 +| `port` | `8097` | Port the local server listens on. |
152 +| `host` | `"localhost"` | Host the local server binds to. |
153 | `httpsOptions` | | _Optional_ object defining `key` and `cert` strings. |
154 | `loggerOptions` | | _Optional_ object defining a `surface` string (to be included with DevTools logging events). |
155 +| `path` | | _Optional_ path to append to the WebSocket URI served to connecting clients (e.g. `"/__react_devtools__/"`). Also set via the `REACT_DEVTOOLS_PATH` env var in the Electron app. |
156 +| `clientOptions` | | _Optional_ object with client-facing overrides (see below). |
157 +
158 +##### `clientOptions`
159 +
160 +When connecting through a reverse proxy, the client may need to connect to a different host, port, or protocol than the local server. Use `clientOptions` to override what appears in the `connectToDevTools()` script served to clients. Any field not set falls back to the corresponding server value.
161 +
162 +| Field | Default | Description |
163 +|---|---|---|
164 +| `host` | server `host` | Host the client connects to. |
165 +| `port` | server `port` | Port the client connects to. |
166 +| `useHttps` | server `useHttps` | Whether the client should use `wss://`. |
167 +
168 +These can also be set via environment variables in the Electron app:
169 +
170 +| Env Var | Description |
171 +|---|---|
172 +| `REACT_DEVTOOLS_CLIENT_HOST` | Overrides the host in the served client script. |
173 +| `REACT_DEVTOOLS_CLIENT_PORT` | Overrides the port in the served client script. |
174 +| `REACT_DEVTOOLS_CLIENT_USE_HTTPS` | Set to `"true"` to make the served client script use `wss://`. |
175 +
176 +##### Reverse proxy example
177 +
178 +Run DevTools locally on the default port, but tell clients to connect through a remote proxy:
179 +```sh
180 +REACT_DEVTOOLS_CLIENT_HOST=remote.example.com \
181 +REACT_DEVTOOLS_CLIENT_PORT=443 \
182 +REACT_DEVTOOLS_CLIENT_USE_HTTPS=true \
183 +REACT_DEVTOOLS_PATH=/__react_devtools__/ \
184 +react-devtools
185 +```
186 +The server listens on `localhost:8097`. The served script tells clients:
187 +```js
188 +connectToDevTools({host: 'remote.example.com', port: 443, useHttps: true, path: '/__react_devtools__/'})
189 +```
190
191 # Development
192
packages/react-devtools-core/src/backend.js
+4 -1
@@ -33,6 +33,7 @@ import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeS
33 type ConnectOptions = {
34 host?: string,
35 nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
36 + path?: string,
37 port?: number,
38 useHttps?: boolean,
39 resolveRNStyle?: ResolveNativeStyle,
@@ -93,6 +94,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
94 const {
95 host = 'localhost',
96 nativeStyleEditorValidAttributes,
97 + path = '',
98 useHttps = false,
99 port = 8097,
100 websocket,
@@ -107,6 +109,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
109 } = options || {};
110
111 const protocol = useHttps ? 'wss' : 'ws';
112 + const prefixedPath = path !== '' && !path.startsWith('/') ? '/' + path : path;
113 let retryTimeoutID: TimeoutID | null = null;
114
115 function scheduleRetry() {
@@ -129,7 +132,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
132 let bridge: BackendBridge | null = null;
133
134 const messageListeners = [];
132 - const uri = protocol + '://' + host + ':' + port;
135 + const uri = protocol + '://' + host + ':' + port + prefixedPath;
136
137 // If existing websocket is passed, use it.
138 // This is necessary to support our custom integrations.
packages/react-devtools-core/src/standalone.js
+41 -5
@@ -306,11 +306,19 @@ type LoggerOptions = {
306 surface?: ?string,
307 };
308
309 +type ClientOptions = {
310 + host?: string,
311 + port?: number,
312 + useHttps?: boolean,
313 +};
314 +
315 function startServer(
316 port: number = 8097,
317 host: string = 'localhost',
318 httpsOptions?: ServerOptions,
319 loggerOptions?: LoggerOptions,
320 + path?: string,
321 + clientOptions?: ClientOptions,
322 ): {close(): void} {
323 registerDevToolsEventLogger(loggerOptions?.surface ?? 'standalone');
324
@@ -345,7 +353,18 @@ function startServer(
353 server.on('error', (event: $FlowFixMe) => {
354 onError(event);
355 log.error('Failed to start the DevTools server', event);
348 - startServerTimeoutID = setTimeout(() => startServer(port), 1000);
356 + startServerTimeoutID = setTimeout(
357 + () =>
358 + startServer(
359 + port,
360 + host,
361 + httpsOptions,
362 + loggerOptions,
363 + path,
364 + clientOptions,
365 + ),
366 + 1000,
367 + );
368 });
369
370 httpServer.on('request', (request: $FlowFixMe, response: $FlowFixMe) => {
@@ -358,15 +377,21 @@ function startServer(
377 // This will ensure that saved filters are shared across different web pages.
378 const componentFiltersString = JSON.stringify(getSavedComponentFilters());
379
380 + // Client overrides: when connecting through a reverse proxy, the client
381 + // may need to connect to a different host/port/protocol than the server.
382 + const clientHost = clientOptions?.host ?? host;
383 + const clientPort = clientOptions?.port ?? port;
384 + const clientUseHttps = clientOptions?.useHttps ?? useHttps;
385 +
386 response.end(
387 backendFile.toString() +
388 '\n;' +
389 `var ReactDevToolsBackend = typeof ReactDevToolsBackend !== "undefined" ? ReactDevToolsBackend : require("ReactDevToolsBackend");\n` +
390 `ReactDevToolsBackend.initialize(undefined, undefined, undefined, ${componentFiltersString});` +
391 '\n' +
367 - `ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${
368 - useHttps ? 'true' : 'false'
369 - }});
392 + `ReactDevToolsBackend.connectToDevTools({port: ${clientPort}, host: '${clientHost}', useHttps: ${
393 + clientUseHttps ? 'true' : 'false'
394 + }${path != null ? `, path: '${path}'` : ''}});
395 `,
396 );
397 });
@@ -374,7 +399,18 @@ function startServer(
399 httpServer.on('error', (event: $FlowFixMe) => {
400 onError(event);
401 statusListener('Failed to start the server.', 'error');
377 - startServerTimeoutID = setTimeout(() => startServer(port), 1000);
402 + startServerTimeoutID = setTimeout(
403 + () =>
404 + startServer(
405 + port,
406 + host,
407 + httpsOptions,
408 + loggerOptions,
409 + path,
410 + clientOptions,
411 + ),
412 + 1000,
413 + );
414 });
415
416 httpServer.listen(port, () => {
packages/react-devtools/README.md
+25 -1
@@ -87,7 +87,31 @@ This will ensure the developer tools are connected. **Don’t forget to remove i
87
88 ## Advanced
89
90 -By default DevTools listen to port `8097` on `localhost`. The port can be modified by setting the `REACT_DEVTOOLS_PORT` environment variable. If you need to further customize host, port, or other settings, see the `react-devtools-core` package instead.
90 +By default DevTools listen to port `8097` on `localhost`. If you need to customize the server or client connection settings, the following environment variables are available:
91 +
92 +| Env Var | Default | Description |
93 +|---|---|---|
94 +| `HOST` | `"localhost"` | Host the local server binds to. |
95 +| `PORT` | `8097` | Port the local server listens on. |
96 +| `REACT_DEVTOOLS_PORT` | | Alias for `PORT`. Takes precedence if both are set. |
97 +| `KEY` | | Path to an SSL key file. Enables HTTPS when set alongside `CERT`. |
98 +| `CERT` | | Path to an SSL certificate file. Enables HTTPS when set alongside `KEY`. |
99 +| `REACT_DEVTOOLS_PATH` | | Path appended to the WebSocket URI served to clients (e.g. `/__react_devtools__/`). |
100 +| `REACT_DEVTOOLS_CLIENT_HOST` | `HOST` | Overrides the host in the script served to connecting clients. |
101 +| `REACT_DEVTOOLS_CLIENT_PORT` | `PORT` | Overrides the port in the script served to connecting clients. |
102 +| `REACT_DEVTOOLS_CLIENT_USE_HTTPS` | | Set to `"true"` to make the served client script use `wss://`. |
103 +
104 +When connecting through a reverse proxy, use the `REACT_DEVTOOLS_CLIENT_*` variables to tell clients to connect to a different host/port/protocol than the local server:
105 +
106 +```sh
107 +REACT_DEVTOOLS_CLIENT_HOST=remote.example.com \
108 +REACT_DEVTOOLS_CLIENT_PORT=443 \
109 +REACT_DEVTOOLS_CLIENT_USE_HTTPS=true \
110 +REACT_DEVTOOLS_PATH=/__react_devtools__/ \
111 +react-devtools
112 +```
113 +
114 +For more details, see the [`react-devtools-core` documentation](https://github.com/facebook/react/tree/main/packages/react-devtools-core).
115
116 ## FAQ
117
packages/react-devtools/app.html
+12 -5
@@ -158,12 +158,19 @@
158 <script>
159 // window.api is defined in preload.js
160 const {electron, readEnv, ip, getDevTools} = window.api;
161 - const {options, useHttps, host, protocol, port} = readEnv();
161 + const {options, useHttps, host, protocol, port, path, clientHost, clientPort, clientUseHttps} = readEnv();
162
163 const localIp = ip.address();
164 - const defaultPort = (port === 443 && useHttps) || (port === 80 && !useHttps);
165 - const server = defaultPort ? `${protocol}://${host}` : `${protocol}://${host}:${port}`;
166 - const serverIp = defaultPort ? `${protocol}://${localIp}` : `${protocol}://${localIp}:${port}`;
164 +
165 + // Effective values for display URLs: client overrides take precedence over server values.
166 + const effectiveHost = clientHost != null ? clientHost : host;
167 + const effectivePort = clientPort != null ? clientPort : port;
168 + const effectiveUseHttps = clientUseHttps != null ? clientUseHttps : useHttps;
169 + const effectiveProtocol = effectiveUseHttps ? 'https' : 'http';
170 + const defaultPort = (effectivePort === 443 && effectiveUseHttps) || (effectivePort === 80 && !effectiveUseHttps);
171 + const pathStr = path != null ? path : '';
172 + const server = defaultPort ? `${effectiveProtocol}://${effectiveHost}${pathStr}` : `${effectiveProtocol}://${effectiveHost}:${effectivePort}${pathStr}`;
173 + const serverIp = defaultPort ? `${effectiveProtocol}://${localIp}${pathStr}` : `${effectiveProtocol}://${localIp}:${effectivePort}${pathStr}`;
174 const $ = document.querySelector.bind(document);
175
176 let timeoutID;
@@ -234,7 +241,7 @@
241 element.innerText = status;
242 }
243 })
237 - .startServer(port, host, options);
244 + .startServer(port, host, options, undefined, path, {host: clientHost, port: clientPort, useHttps: clientUseHttps});
245 </script>
246 </body>
247 </html>
packages/react-devtools/preload.js
+18 -1
@@ -36,6 +36,23 @@ contextBridge.exposeInMainWorld('api', {
36 const host = process.env.HOST || 'localhost';
37 const protocol = useHttps ? 'https' : 'http';
38 const port = +process.env.REACT_DEVTOOLS_PORT || +process.env.PORT || 8097;
39 - return {options, useHttps, host, protocol, port};
39 + const path = process.env.REACT_DEVTOOLS_PATH || undefined;
40 + const clientHost = process.env.REACT_DEVTOOLS_CLIENT_HOST || undefined;
41 + const clientPort = process.env.REACT_DEVTOOLS_CLIENT_PORT
42 + ? +process.env.REACT_DEVTOOLS_CLIENT_PORT
43 + : undefined;
44 + const clientUseHttps =
45 + process.env.REACT_DEVTOOLS_CLIENT_USE_HTTPS === 'true' ? true : undefined;
46 + return {
47 + options,
48 + useHttps,
49 + host,
50 + protocol,
51 + port,
52 + path,
53 + clientHost,
54 + clientPort,
55 + clientUseHttps,
56 + };
57 },
58 });