fix: remove stale data listener explicitly on socket error by Chrisp1tv · Pull Request #3209 · redis/node-redis

Description

Fix a race condition in RedisSocket where buffered data events from a broken socket could be emitted after a reconnect had already started, causing stale responses to be matched to new commands.

This PR tries to provide a fix for the bug reported in:

I must give some credits to @nicklvsa for the reproduction script I've been using to test the issue and my fixes. Thanks for this 🙏

Root cause (written by Claude):

RedisSocket attaches a persistent data listener to each TCP socket it creates. When a socket error occurs, #onSocketError flushes the command queue and resets the RESP decoder — but the old socket was not destroyed immediately, leaving its data listener alive. Any responses already buffered in the kernel for the old socket would then fire, get parsed by the reset decoder, and be matched against new commands in the queue. This causes silent data corruption: GET key1 could resolve with the value of key2.

This fix:

  • Captures and clears this.#socket before emitting error, so the reference is atomically transferred to a local variable before any listener runs
  • Calls socket.removeAllListeners('data') before socket.destroy(), ensuring no buffered data events from the old socket reach the decoder after teardown
  • Calls socket.destroy() before the reconnect

Checklist

  • Does npm test pass with this change (including linting)?
  • Is the new or changed code fully tested?
  • Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?

Note

Medium Risk
Touches core socket error/reconnect handling; while small, it can affect connection teardown timing and event ordering during failures/reconnects.

Overview
Fixes a race in RedisSocket teardown on errors by atomically clearing this.#socket before emitting the error event, and then explicitly removing the old socket’s data listeners before destroying it.

Also hardens the initiator error path by using this.#socket?.destroy() to avoid crashing if the socket reference has already been cleared.

Written by Cursor Bugbot for commit c6b6288. This will update automatically on new commits. Configure here.