Skip to content

Join catch-up updatePadClients() in handleClientReady races with queued USER_CHANGES, resulting in "Failed to apply USER_CHANGES" #8387

Closed

Description

@MaBeFHDW

Describe the bug
Since #7480, handleClientReady() ends with await exports.updatePadClients(pad); (src/node/handler/PadMessageHandler.ts; v3.3.9 / develop a2cfa88a5, ~L1503). updatePadClients(pad) walks all sockets of the pad and advances each sessioninfo.rev up to the head revision. This call runs outside the per-pad padChannels queue that serializes handleUserChanges(). This function appends the revision and then, after further awaits, checks assert.equal(thisSession.rev, r);.

If another client joins the pad in that window, the join catch-up sends the author's own new revision to them as NEW_CHANGES and moves thisSession.rev to r + 1. This causes the assert to fail, and the server sends {disconnect: 'badChangeset'}. The author's client reconnects, and this reconnect is itself a join that can hit the next writer. With many people in one pad this snowballs into a reconnect cascade (lost keystrokes, dead clicks, users constantly rejoining).

To Reproduce
This has been tested without plugins on default settings. Claude Code has generated a testing file called joinCatchUpRace.ts (drop into src/tests/backend/specs/) (attached to this issue) to reproduce the bug. It uses the existing backend test helpers (tests/backend/common.ts), so it runs inside an Etherpad checkout like the other backend specs.

Steps to reproduce the behavior:

  1. Client A joins the pad and sends a USER_CHANGES.
  2. The test holds A's handleUserChanges() right after pad.appendRevision() (wrapping appendRevision on the pad instance).
  3. Client B joins the pad meanwhile; its catch-up updatePadClients() runs.
  4. A's change is released. Expected ACCEPT_COMMIT, actual: AssertionError: writer got ["NEW_CHANGES","USER_NEWINFO","disconnect:badChangeset"], and the server logs: [WARN] message - Failed to apply USER_CHANGES from author a.authorid (socket socketid) on pad padid: AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 2 !== 1

In production we see the same log line many times per session with ~10–20 people in one pad. Every occurrence coincides within ~100 ms with a join on the same pad, and each failure triggers a reconnect that is again a join.

Expected behavior
A joining client must not change the revision state of other sessions. Only the joining socket should be caught up.

Server:

  • Etherpad version: 3.3.8 (identical code in 3.3.9 and develop)
  • OS: Debian 12
  • Node.js version: as shipped in the official Docker image
  • npm version: as shipped in the official Docker image
  • Is the server free of plugins: the production instance is not plugin-free, it runs about 16 plugins, of which none hook into the affected code path. Claude Code has verified this bug on a plugin-free testing instance.
  • Are you using any abstraction IE docker? Yes, the official image, inside rootless Docker, behind an Apache reverse proxy.

Desktop:

  • Mixed. We were using different devices and operating systems (Windows 11, Linux (Ubuntu, Fedora, Artix)) and different browsers. This is a browser-independent bug.

Suggested fix:
Claude suggests to fix this bug by changing two places in PadMessageHandler.ts:

-exports.updatePadClients = async (pad: PadType) => {
+exports.updatePadClients = async (pad: PadType, onlySocketId?: string) => {
   // skip this if no-one is on this pad
-  const roomSockets = _getRoomSockets(pad.id);
+  let roomSockets = _getRoomSockets(pad.id);
+  if (onlySocketId != null) roomSockets = roomSockets.filter((s) => s.id === onlySocketId);
   if (roomSockets.length === 0) return;
@@ handleClientReady
-    await exports.updatePadClients(pad);
+    await exports.updatePadClients(pad, socket.id);

With this change the test passes and the full backend suite stays green (1875 passing). Running the catch-up through the pad’s padChannels queue would be a more thorough alternative.

Additional context

joinCatchUpRace.ts.txt

Activity

  1. Raj85446696 commented on Oct 31, 2026

    @Raj85446696
    Contributor

    i fix the issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions