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.52%. Comparing base (17117f6) to head (508ae44).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5713      +/-   ##
==========================================
+ Coverage   93.50%   93.52%   +0.01%     
==========================================
  Files         110      110              
  Lines       39072    39095      +23     
==========================================
+ Hits        36534    36562      +28     
+ Misses       2538     2533       -5     

☔ 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.

@pacocartones
pacocartones force-pushed the fix/websocketstream-abort-listener-leak branch 2 times, most recently from cd86efd to 32e5f75 Compare September 2, 2026 05:33
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 32e5f75 to 508ae44 Compare September 4, 2026 02:28
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