Skip to content

[Android] Heap snapshot via Hermes CDP fails mid-stream: OkHttp WebSocket closes with code 1001 due to MAX_QUEUE_SIZE overflow in CxxInspectorPackagerConnection #56471

@ncpa0cpl

Description

@ncpa0cpl

Description

The following issue was found using the Claude Opus 4.6 LLM and verified by me manually. I've used Claude to come up with a fix in the React Native Java code, then monkey patched the RN source files and built an app with that fix.

Before the fix whenever I tried to get the snapshot through the Debugger, Chrome Dev Tools or even a custom Node script it would fail. When using the node script I would get around 70MB of snapshot and then the WS connection would close leaving me with an incomplete snapshot file. By inspecting the internal metro code during that I found that the socket connection to the device was closing with the 1001 code.

After applying the fix Claude came up with both the Debugger and the Node script were able to get the snapshot without any issues.

The final snapshot I extracted from the app was around 120 MB.

I've confirmed this issue to be present in the 0.83. which is the latest version I am able to upgrade the project in which I ran into this issue. The fix that ended up helping I only validated on 0.79.6

Summary generated by Claude

React Native version: 0.79.6 (unpatched as of main branch)

Platform: Android only

Description

Taking a JS heap snapshot from a React Native Android app via Hermes CDP always fails for large heaps (>100MB snapshot size). The WebSocket connection between the app and Metro closes with code 1001 partway through the snapshot — typically after a portion of chunks are delivered — causing the snapshot to be incomplete or the tooling to throw WS closed before snapshot read was completed.

Root cause

CxxInspectorPackagerConnection.java uses OkHttp to maintain the WebSocket from the app to Metro. OkHttp's RealWebSocket has a hardcoded MAX_QUEUE_SIZE = 16 * 1024 * 1024 (16 MiB). When HeapProfiler.takeHeapSnapshot is triggered, Hermes streams chunks to the inspector faster than TCP delivers them to Metro. The outgoing OkHttp queue fills past 16 MiB, and OkHttp closes the connection with code 1001.

A second issue compounds it: the OkHttpClient is configured with writeTimeout(10, TimeUnit.SECONDS). Since heap snapshots can take more than 10 seconds to fully flush, the write timeout can also trigger disconnection independently.

Relevant file: ReactAndroid/src/main/java/com/facebook/react/devsupport/CxxInspectorPackagerConnection.java

// Current code (unfixed):

private final OkHttpClient mHttpClient =                                                                                               
      new OkHttpClient.Builder()
          .connectTimeout(10, TimeUnit.SECONDS)                                                                                          
          .writeTimeout(10, TimeUnit.SECONDS)  // ← kills long snapshots
          .readTimeout(0, TimeUnit.MINUTES)                                                                                              
          .build();                                         

// ...in connectWebSocket():

  return new IWebSocket() {
      @Override                                                                                                                          
      public void send(String message) {
          webSocket.send(message);  // ← no backpressure, overflows MAX_QUEUE_SIZE                                                       
      }                                                                                                                                  
      // ...
  };                                                                                                                                     

Fix

Two changes in connectWebSocket():

  1. Set writeTimeout(0, TimeUnit.SECONDS) to disable the write timeout (matching how readTimeout is already disabled).
  2. Introduce an unbounded local queue with a sender thread that throttles sends to keep OkHttp's internal queue below its 16 MiB hard limit:
  .writeTimeout(0, TimeUnit.SECONDS) // was 10s — disabled to match readTimeout                                                          
                                                                                                                                         
  // In connectWebSocket(), replace direct webSocket.send() with:
  final BlockingQueue<String> localQueue = new LinkedBlockingQueue<>();                                                                  
  final AtomicBoolean closing = new AtomicBoolean(false);                                                                                
  final long OKHTTP_QUEUE_HIGH_WATERMARK = 8 * 1024 * 1024; // stay under 16 MiB hard limit                                              
                                                                                                                                         
  Thread senderThread = new Thread(() -> {                                                                                               
      while (!closing.get()) {                                                                                                           
          try {                                             
              String message = localQueue.poll(1, TimeUnit.SECONDS);
              if (message == null) continue;                                                                                             
              while (webSocket.queueSize() > OKHTTP_QUEUE_HIGH_WATERMARK) {
                  Thread.sleep(5);                                                                                                       
              }                                                                                                                          
              webSocket.send(message);
          } catch (InterruptedException e) {                                                                                             
              Thread.currentThread().interrupt();           
              break;
          }
      }
  }, "InspectorWebSocketSender");
  senderThread.setDaemon(true);                                                                                                          
  senderThread.start();
                                                                                                                                         
  return new IWebSocket() {                                 
      @Override public void send(String message) { localQueue.offer(message); }
      @Override public void close() {                                                                                                    
          closing.set(true);
          webSocket.close(1000, "End of session");                                                                                       
      }                                                                                                                                  
  };

Steps to reproduce

  1. Run a React Native Android app in debug mode with a large heap
  2. Connect to Metro via CDP and call HeapProfiler.takeHeapSnapshot
  3. Observe connection closes before the whole snapshot is delivered. code 1001 is given by the socket (node_modules/@react-native/dev-middleware/dist/inspector-proxy/InspectorProxy.js:232)

React Native Version

0.83.0

Output of npx @react-native-community/cli info

System:
  OS: Linux 6.18 Manjaro Linux
  CPU: (8) x64 Intel(R) Core(TM) i7-10610U CPU @ 1.80GHz
  Memory: 2.76 GB / 30.97 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 22.22.0
    path: /home/owner/.nvm/versions/node/v22.22.0/bin/node
  Yarn:
    version: 1.22.22
    path: /usr/local/bin/yarn
  npm:
    version: 10.9.4
    path: /home/owner/.nvm/versions/node/v22.22.0/bin/npm
  Watchman: Not Found
SDKs:
  Android SDK:
    Android NDK: 29.0.14206865
IDEs:
  Android Studio: AI-253.30387.90.2532.14935130
Languages:
  Java:
    version: 25.0.2
    path: /usr/bin/javac
  Ruby:
    version: 3.4.8
    path: /usr/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 19.2.0
    wanted: 19.2.0
  react-native:
    installed: 0.83.0
    wanted: 0.83.0
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: Not found
  newArchEnabled: Not found
iOS:
  hermesEnabled: Not found
  newArchEnabled: Not found

Screenshots and Videos

No response

Maybe related to
#39651
#49158

Repro

This would likely be possible to be reproduced with this: https://github.com/abbasvlb/heapdumpissue/

although I would use a higher value than 20MB

Metadata

Metadata

Assignees

No one assigned

    Labels

    DebuggingIssues related to React Native DevTools or legacy JavaScript/Hermes debuggingFlowNeeds: Author FeedbackNeeds: ReproThis issue could be improved with a clear list of steps to reproduce the issue.Platform: AndroidAndroid applications.🌐NetworkingRelated to a networking API.📦Bundler

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions