Skip to content

Fix stuck WebSocket reconnect and connection ownership - #4

Merged
dezinezync merged 1 commit into
DZNS:mainfrom
Jatin5120:fix/websocket-reconnect-ownership
Oct 13, 2026
Merged

dezinezync merged 1 commit into
DZNS:mainfrom
Jatin5120:fix/websocket-reconnect-ownership

Conversation

@Jatin5120

Copy link
Copy Markdown
Contributor

Problem

When a reconnect attempt failed, handleDisconnection() returned early, because its guard only accepted .connected or .connecting and a retry runs in .reconnecting. Nothing scheduled the next attempt, so the client stayed in .reconnecting forever and any send waiting for the connection never resumed.

Callbacks, the retry timer, and the receive and ping loops also had no link to the connection that started them, so events from an old connection could affect a new one.

Changes

All changes are in DZWebSocketClient.swift.

  • A failed retry now schedules the next one, until the policy's limit is reached.
  • The retry timer is stored, so connect() and disconnect() can cancel it. An old timer can no longer start an attempt in a later cycle.
  • connect() does nothing while a retry handshake is in progress. During the wait between retries, it retries right away without resetting the attempt count.
  • Every delegate callback passes its own task, and callbacks from a replaced task are ignored. A close followed by a completion for the same failure counts as one failure. onError still reports every error.
  • The ping and receive loops stop as soon as the connection drops. Late messages and ping errors from an old connection are ignored.
  • No Task is created for a state change when no onStateChange handler is set.

Compatibility

  • There are no public API changes; swift package diagnose-api-breaking-changes main reports none.
  • ReconnectionPolicy is unchanged.
  • Sending works the same as before.
  • With no policy, the client keeps retrying every 2s until disconnect() is called. Passing ReconnectionPolicy() gives a finite backoff.

Testing

  • All 47 existing tests pass, with no test changes. I ran them with swift test on macOS and on the iOS 26.5 simulator.
  • I compared main and this branch using local test servers. These scenarios fail on main and pass with this change:
    • retries reach the policy limit, with no policy and with exponential or linear backoff
    • the client recovers after the server comes back
    • repeated connect() calls start no extra handshakes
    • an old timer, a late open/close/completion, a held message or a ping error from a replaced connection causes no extra attempts and doesn't break the new connection

Known issues, for a follow-up PR

  • testTaskCancellation is flaky on main too (about 1 in 10 runs). In send(message:responseType:), the cancel Task can reach the actor before the request is registered, so a cancelled request can still return successfully.
  • After the retry limit is reached, send returns without sending, and a pending request can wait indefinitely.

🤖 Generated with Claude Code

A failed retry was ignored while the client was reconnecting, so the
client stayed in .reconnecting forever. Callbacks, the retry timer, and
the receive and ping loops also had no link to the connection that
started them, so events from an old connection could affect a new one.

- Schedule the next retry when a retry fails, up to the policy limit
- Store the retry timer so connect() and disconnect() can cancel it
- Ignore connect() while a retry handshake is in progress
- Pass the originating task to every callback and ignore old ones
- Count close and completion for the same failure once
- Stop ping and receive loops as soon as a connection drops
- Skip the state-change Task when no handler is set

Public API, policy calculations, error reporting and send behavior are
unchanged. All existing tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dezinezync
dezinezync merged commit 8d30524 into DZNS:main Oct 13, 2026
3 checks passed
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