Skip to content

fix(websocket): remove WebSocketStream handshake abort listener after the handshake - #5713

Open
pacocartones wants to merge 1 commit into
nodejs:mainfrom
pacocartones:fix/websocketstream-abort-listener-leak
Open

fix(websocket): remove WebSocketStream handshake abort listener after the handshake#5713
pacocartones wants to merge 1 commit into
nodejs:mainfrom
pacocartones:fix/websocketstream-abort-listener-leak

Conversation

@pacocartones

Copy link
Copy Markdown
Contributor

What

WebSocketStream attaches an abort listener to the caller's AbortSignal during the opening handshake (addAbortListener at lib/web/websocket/stream/websocketstream.js:136) but never removes it.

Why

Once the connection is established the listener becomes a no-op (it checks isEstablished and does nothing), yet it stays attached to the signal. Because the listener closure captures the WebSocketStream instance, a long-lived signal — for example an app-level shutdown AbortController reused across streams — keeps every already-closed WebSocketStream alive. That is a memory leak, and it surfaces as a MaxListenersExceededWarning once more than 10 streams share one signal.

How

Store the disposable returned by addAbortListener and remove it once the handshake concludes: on #onConnectionEstablished, on #onSocketClose, and after the abort steps run. The removal helper is idempotent.

Test

Adds test/websocket/stream/abort-listener-leak.js, asserting the signal has zero abort listeners after both a clean close and an aborted handshake. It fails before the change (1 !== 0) and passes after. The existing test/websocket/stream/* suite stays green (6 passing); lint clean.

Reachability

new WebSocketStream(url, { signal }) is ordinary use of a public API. Any consumer that reuses a signal across streams triggers the leak.

@codecov-commenter

codecov-commenter commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.46%. Comparing base (5033aa1) to head (cd86efd).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5713      +/-   ##
==========================================
- Coverage   93.47%   93.46%   -0.01%     
==========================================
  Files         110      110              
  Lines       38906    38931      +25     
==========================================
+ Hits        36366    36388      +22     
- Misses       2540     2543       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

A WebSocketStream created with a signal added an abort listener during the

opening handshake but never removed it. After the connection was

established (or closed) the listener became a no-op yet stayed attached to

the signal, so a long-lived signal kept every closed WebSocketStream and

its listener closure alive, leaking memory and eventually triggering a

MaxListenersExceededWarning when a signal is reused across streams.

Remove the listener once the handshake concludes (on established, on

socket close, and after the abort steps run).

Assisted-by: Claude Opus 4.8
Signed-off-by: Paco Cartones <pacocartones@users.noreply.github.com>
@pacocartones
pacocartones force-pushed the fix/websocketstream-abort-listener-leak branch from 7e61b7a to cd86efd Compare August 28, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants