Skip to content

fix: prevent auto-reconnect during QR pairing phase - #192

Open
RoushanKhalid wants to merge 1 commit into
evolution-foundation:mainfrom
RoushanKhalid:fix/qr-pairing-disconnect-race
Open

fix: prevent auto-reconnect during QR pairing phase#192
RoushanKhalid wants to merge 1 commit into
evolution-foundation:mainfrom
RoushanKhalid:fix/qr-pairing-disconnect-race

Conversation

@RoushanKhalid

@RoushanKhalid RoushanKhalid commented Sep 9, 2026

Copy link
Copy Markdown

Description

*events.Disconnected in myEventHandler unconditionally spawns a goroutine that calls ReconnectClient(), regardless of whether the device is still mid-pairing (Store.ID == nil) or already logged in.

During new-device QR pairing, Disconnected fires routinely and repeatedly — WhatsApp's servers cycle the socket several times before a device is actually paired — which is expected and already handled correctly by handleQRCodes/teardownQR's own rotation timer (60s for the first code, 20s for each of the rest, 5-code max ≈ 140s total).

The problem is that ReconnectClient() also fires on every one of those routine pairing-phase disconnects, concurrently with the QR-rotation goroutine that's already running. Both paths touch the same unsynchronized qrcodeCount / instance maps, and ReconnectClient() unconditionally tears the client down and restarts it from scratch ("No jid found. Creating new device"). In practice this collapses the intended ~140s pairing window down to a few seconds — the max QR count gets hit almost immediately, well before a code can realistically be scanned, and the whole cycle repeats indefinitely.

Related Issue

None filed yet — happy to open one and link it if preferred.

Root cause (reproduced)

Captured logs from a real pairing attempt show the QR counter reaching its max (5) and forcing a logout/restart within a few seconds of StartClient, instead of the ~140s the rotation timer implies:

02:06:50 QR code generated #3 (max: 5)
02:06:53 QR code generated #3 (max: 5)   <- same count logged twice, back-to-back
02:06:56 QR code generated #3 (max: 5)
02:06:58 Disconnected detected, restarting instance
02:06:58 Starting reconnection process - simulating restart
...
02:07:01 Starting fresh instance
02:07:02 QR code generated #1 (max: 5)   <- device reset, counter restarts
02:07:02 QR code generated #2 (max: 5)
02:07:03 QR code generated #2 (max: 5)   <- duplicate count again
02:07:07 QR code generated #5 (max: 5)
02:07:07 Maximum QR code count reached (5), forcing logout and QRTimeout

Fix

Only auto-reconnect on *events.Disconnected once the device is already paired (mycli.WAClient.Store.ID != nil). A disconnect that happens while still mid-QR-pairing is left entirely to the QR-rotation lifecycle that already owns it — this preserves the auto-heal behavior for the case it looks like it was written for (a real, already-paired session dropping), without it also firing on every pairing-phase blip.

After the fix, the same pairing attempt rotates cleanly on the documented schedule with no spurious restarts:

02:20:05 QR code generated #2 (max: 5)
02:20:25 QR code generated #3 (max: 5)   <- exactly 20s later
02:20:45 QR code generated #4 (max: 5)   <- exactly 20s later
02:21:05 QR code generated #5 (max: 5)   <- exactly 20s later, max reached as designed

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Testing

  • Manual testing completed
  • Functionality verified in development environment
  • No breaking changes introduced

Verified against a real pairing attempt (scanned QR successfully, Store.ID set) and a real message send/delivery/read-receipt afterward, confirming the already-paired auto-reconnect path is unaffected. gofmt-clean diff, no other files touched.

Additional Notes

Found while integrating Evolution Go's WhatsApp gateway into a separate application — QR pairing was effectively non-functional before this fix (every attempt hit the race within a few seconds of the first code). Happy to adjust the approach if there's a preferred way to guard this instead.

Summary by Sourcery

Keep QR pairing stable by separating pairing-phase recovery from post-pairing session reconnection.

Bug Fixes:

  • Prevent QR pairing from being disrupted by destructive auto-reconnects triggered by routine disconnect events.
  • Recover genuine pairing-phase connection drops by reconnecting the existing client session without resetting device state.

Enhancements:

  • Preserve automatic reconnection for disconnects occurring after the device has been paired.

@sourcery-ai

sourcery-ai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Reviewer's Guide

Prevents routine disconnects during QR pairing from racing the QR rotation lifecycle and resetting the client, while preserving automatic recovery for already-paired sessions.

Sequence diagram for pairing-aware disconnect handling

sequenceDiagram
    participant WA as WhatsApp
    participant Handler as myEventHandler
    participant QR as QR rotation lifecycle
    participant Service as ReconnectClient

    WA->>Handler: Disconnected
    alt Store.ID is nil
        Handler->>QR: Leave restart lifecycle unchanged
        QR->>QR: Rotate QR on 60s/20s timer
    else Store.ID is set
        Handler->>Service: ReconnectClient(instanceID)
        Service->>Service: Restart paired client
    end
Loading

File-Level Changes

Change Details Files
Restrict automatic reconnects to established sessions so QR pairing remains controlled by its existing rotation lifecycle.
  • Check that the WhatsApp store, client, and paired-device ID are present before spawning reconnect work.
  • Leave pairing-phase disconnects to the QR rotation/teardown timers and log that decision.
  • Preserve asynchronous reconnect and error logging for already-paired devices.
pkg/whatsmeow/service/whatsmeow.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="pkg/whatsmeow/service/whatsmeow.go" line_range="2000-2009" />
<code_context>
+		// this on Store.ID keeps the auto-heal for a real mid-session drop
+		// (the case this was written for) without it also firing on every
+		// pairing-phase blip.
+		if mycli.WAClient != nil && mycli.WAClient.Store.ID != nil {
+			go func(instanceID string) {
+				mycli.loggerWrapper.GetLogger(instanceID).LogInfo("[%s] Disconnected detected, restarting instance", instanceID)
+				if err := mycli.service.ReconnectClient(instanceID); err != nil {
+					mycli.loggerWrapper.GetLogger(instanceID).LogError("[%s] Failed to restart instance: %v", instanceID, err)
+				}
+			}(mycli.userID)
+		} else {
+			mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Disconnected during QR pairing -- leaving restart to the QR-rotation lifecycle, not auto-reconnecting", mycli.userID)
+		}
 	case *events.LabelEdit:
 		doWebhook = true
</code_context>
<issue_to_address>
**issue (broader_impact):** When a new-device client emits `*events.Disconnected` while `Store.ID` is nil, this branch does not reconnect it. `StartClient` has disabled `EnableAutoReconnect`, and the QR rotation goroutine only waits, publishes the next QR, and eventually signals teardown; it never calls `Connect`, so a routine socket closure leaves the pairing socket dead until the QR timeout/restart.

**Triggers:** When WhatsApp closes the websocket during QR pairing before the device has been paired.

**Suggested fix:** Have the QR-pairing lifecycle explicitly reconnect the existing client after a transient disconnect, or ensure the client remains/reconnects connected while QR rotation is active; do not simply suppress `ReconnectClient` without replacing its connection-recovery behavior.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: pkg/whatsmeow/service/whatsmeow.go:2009


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread pkg/whatsmeow/service/whatsmeow.go
@RoushanKhalid
RoushanKhalid force-pushed the fix/qr-pairing-disconnect-race branch from d3761f4 to e8b782a Compare September 9, 2026 06:14
@RoushanKhalid

Copy link
Copy Markdown
Author

Good catch, thanks — that was a real gap. Pushed an update (force-pushed the same branch, per the contributing guide): a genuinely dropped socket during pairing now gets an explicit reconnect too, it's just a lighter-weight one than before.

Instead of calling ReconnectClient() (which tears the client down and mints a brand-new device identity — the thing that raced the QR-rotation goroutine in the first place), the pairing-phase branch now calls mycli.WAClient.Connect() directly on the same, already-initialized client — the same recovery idiom StartClient's own EOF-retry branch already uses. That reconnects the transport without resetting device/pairing state or touching the shared instance maps / qrcodeCount that caused the original race, so it can't reintroduce it.

Guarded with IsConnected() first so it's a no-op on the (common) case where the socket already recovered by the time the goroutine runs — verified this doesn't fire spuriously during a normal, healthy pairing rotation (retested live: QR codes still rotate cleanly on the documented ~60s/20s schedule, no extra reconnect log lines).

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.

1 participant