react / react/react-native

Dev server exits on any WebSocket error: InspectorProxy never listens for 'error'

オープン 初心者向け
#57,793 コメント 2 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

Needs: Author Feedback Needs: Repro
主要言語
C++
スター
127k
フォーク
25.3k
平均マージ
1日 23時間
マージ済み PR(30日)
4

説明

Description

InspectorProxy never attaches an "error" listener to the WebSocket connections it accepts. In Node, an 'error' event with no listener throws — so a socket-level failure on one connection takes down the entire dev server, not just that connection.

Both connection handlers listen for "message" and "close" only. On main today:

  • packages/dev-middleware/src/inspector-proxy/InspectorProxy.js#L336#createDeviceConnectionWSServer
  • packages/dev-middleware/src/inspector-proxy/InspectorProxy.js#L525#createDebuggerConnectionWSServer

I hit this twice in one working session, roughly 4.5 hours into each run, on an otherwise idle project with a simulator attached. Metro exits with:

RangeError: Too many message fragments
    at Receiver.getData (.../@react-native/dev-middleware/node_modules/ws/lib/receiver.js:359:14)
    at Receiver.startLoop (.../ws/lib/receiver.js:158:22)
    at Receiver._write (.../ws/lib/receiver.js:94:10)
    at Socket.socketOnData (.../ws/lib/websocket.js:882:35)
Emitted 'error' event on WebSocket instance at:
    at Receiver.receiverOnError (.../ws/lib/websocket.js:787:13) {
  [Symbol(status-code)]: 1008
}

Two things are tangled here, and I want to be clear that only one is a bug:

  1. The RangeError itself comes from React Native's vendored, patched ws@6.2.5, which adds caps upstream ws does not have — maxFragments: 16 * 1024 and maxBufferedChunks: 256 * 1024 (ws/lib/websocket.js). That looks like deliberate hardening and I am not suggesting it be relaxed. InspectorProxy sets maxPayload: 0 on both servers but leaves maxFragments at that default, which seems intentional.

  2. The bug is that tripping it is fatal to the process rather than to the connection. The fragment cap is just the error this project happened to hit; any socket-level error on either server has the same effect. A dev server should not be killable by one misbehaving WebSocket peer.

Steps to reproduce

Self-contained, no app and no Expo required:

mkdir repro && cd repro && npm init -y
npm i @react-native/dev-middleware@0.81.5 ws
node repro.js
// repro.js
const http = require('http');
const WS = require('ws');
const {createDevMiddleware} = require('@react-native/dev-middleware');

const server = http.createServer();
const {middleware, websocketEndpoints} = createDevMiddleware({
  projectRoot: __dirname,
  serverBaseUrl: 'http://localhost:8099',
});
server.on('request', middleware);
server.on('upgrade', (req, socket, head) => {
  const {pathname} = new URL(req.url, 'http://localhost');
  const wss = websocketEndpoints[pathname];
  if (!wss) return socket.destroy();
  wss.handleUpgrade(req, socket, head, ws => wss.emit('connection', ws, req));
});

server.listen(8099, () => {
  const ws = new WS(
    'ws://localhost:8099/inspector/device?device=1&name=repro&app=com.example',
  );
  ws.on('open', () => {
    // One message split into more fragments than the vendored ws allows.
    const s = ws._sender;
    s.send(Buffer.from('x'), {fin: false, opcode: 1, mask: true}, () => {});
    for (let i = 0; i < 17000; i++) {
      s.send(Buffer.from('x'), {fin: false, opcode: 0, mask: true}, () => {});
    }
    s.send(Buffer.from('x'), {fin: true, opcode: 0, mask: true}, () => {});
  });
  ws.on('error', () => {});
  setTimeout(() => {
    console.log('PROXY SURVIVED — connection dropped, server still up.');
    process.exit(0);
  }, 6000);
});

Expected: the proxy drops that connection and keeps serving; PROXY SURVIVED prints.

Actual: the process dies with the unhandled 'error' event and the stack above. PROXY SURVIVED never prints.

Note the reproducer forces the error deterministically in seconds rather than waiting hours. In the wild it arrived on its own from a normally-connected iOS simulator.

The fix

Attaching an "error" listener at the top of both connection handlers is enough. It has to be attached synchronously, before the first await — these callbacks are async, so an error arriving mid-await would otherwise still be unhandled:

wss.on('connection', async (socket: WS, req) => {
  socket.on('error', error => {
    this.#logger?.error('Error on device connection, closing it: %s', error?.message ?? String(error));
    // terminate() rather than close(): close() waits for a closing handshake,
    // and a socket that failed mid-frame may never complete one.
    try { socket.terminate(); } catch {}
  });
  // ...

Verified locally as a patch-package patch against 0.81.5: with the listener, the same reproducer prints PROXY SURVIVED, and a real project's Metro then starts, builds its iOS bundle, and serves a connected simulator normally.

Happy to open a PR if that would help.

React Native Version

0.81.5

Output of npx @react-native-community/cli info
System:
  OS: macOS 27.0
  CPU: (12) arm64 Apple M3 Pro
  Memory: 58.52 MB / 18.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 22.12.0
    path: /usr/local/bin/node
  Yarn: Not Found
  npm:
    version: 11.0.0
    path: /usr/local/bin/npm
  Watchman: Not Found
Managers:
  CocoaPods:
    version: 1.17.0
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 25.5
      - iOS 26.5
      - macOS 26.5
      - tvOS 26.5
      - visionOS 26.5
      - watchOS 26.5
  Android SDK: Not Found
IDEs:
  Android Studio: Not Found
  Xcode:
    version: 26.6/17F113
    path: /usr/bin/xcodebuild
Languages:
  Java: Not Found
  Ruby:
    version: 2.6.10
    path: /usr/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 19.1.0
    wanted: 19.1.0
  react-native:
    installed: 0.81.5
    wanted: 0.81.5
  react-native-macos: Not Found
Notes on scope

The project I hit this on uses Expo, and the template asks Expo users to file with Expo first. I have filed here rather than there deliberately: the missing listener is in @react-native/dev-middleware, the reproducer above installs that package directly and involves no Expo code, and the two wss.on('connection', ...) handlers on main still have no "error" listener. Happy to move it if you disagree.

Screenshots and Videos

n/a — the reproducer output above is the whole symptom.

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

packages/dev-middleware/src/inspector-proxy/InspectorProxy.js の 336 行目と 525 行目付近にある接続ハンドラーから始め、それぞれの WebSocket が message イベントと close イベントをどのように処理しているか比較します。issue にある自己完結型の reproducer を実行します。両方の接続パスが socket エラーを処理して dev server を終了させず、reproducer が proxy が存続したことを出力すれば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
javascript, react-native
領域
devtools, tooling
issue の種類
バグ
難易度
2/5
見積もり時間
1〜3時間
活発さ
活発
明瞭さ
明確に書かれている
初心者へのやさしさ
88/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。