Skip to content

fix(instance): do not open a second connection on repeated clicks - #35

Open
michumichifu wants to merge 1 commit into
evolution-foundation:mainfrom
michumichifu:fix/prevent-duplicate-connect
Open

fix(instance): do not open a second connection on repeated clicks#35
michumichifu wants to merge 1 commit into
evolution-foundation:mainfrom
michumichifu:fix/prevent-duplicate-connect

Conversation

@michumichifu

@michumichifu michumichifu commented Aug 7, 2026

Copy link
Copy Markdown

Problem

The QR code and pairing code buttons call handleConnect with no guard against
repeated clicks:

<DialogTrigger onClick={() => handleConnect(instance.name, false)} asChild>
  <Button></Button>
</DialogTrigger>

That matters because GET /instance/connect is not a read-only endpoint. When
the instance is closed it opens a new WhatsApp connection:

o == "open"       ? await this.connectionState({ instanceName: s })
: o == "connecting" ? t.qrCode
: o == "close"    ? (await t.connectToWhatsapp(e), await delay(2000), t.qrCode)

There is a 2-second delay inside that branch before anything is returned, and
the button stays enabled the whole time. Clicking again during that window
starts a second connection for the same instance, so two sockets end up racing:
each produces its own QR code, and the one the user is looking at may be
invalidated by the other. This is easy to trigger by accident, because when the
first attempt seems slow the natural reaction is to click again.

Fix

Allow a single request in flight at a time, and disable the buttons while it
runs.

The guard is a useRef rather than state on purpose: two fast clicks would both
read the same stale value from state before React re-renders, and both would
pass the check. A ref is updated synchronously, so the second click is rejected.

isConnecting state is still used for the disabled prop, which gives the user
visible feedback instead of leaving them wondering whether the click registered.

Notes

  • Applies to both entry points, QR code and pairing code, since they share the
    same handler.
  • The flag is reset in a finally block, so a failed request does not leave the
    buttons permanently disabled.
  • npm run type-check and eslint pass. prettier --check reports this file as
    unformatted both before and after the change, so it was left untouched rather
    than mixing a whole-file reformat into the diff.

Summary by Sourcery

Guard instance connection requests so only one connect operation can run at a time and reflect this state in the UI.

Bug Fixes:

  • Prevent multiple WhatsApp connections and racing QR codes caused by repeated clicks on the connect actions.

Enhancements:

  • Disable QR code and pairing code buttons while a connection request is in progress to provide clear feedback and avoid duplicate actions.

Every click on the QR code / pairing code button called GET /instance/connect
with no guard. When the instance is closed that endpoint does not return a
cached code: it calls connectToWhatsapp() and opens a brand new WhatsApp
connection. Clicking twice therefore left two sockets racing for the same
instance, each producing its own QR code, and the phone could end up scanning
the one that the other socket had already invalidated.

Keep a single request in flight: a ref guards against overlapping calls and is
checked synchronously, since two fast clicks would both pass a state-based
check before React re-renders. The buttons are disabled while the request is
running, so the user gets feedback instead of clicking again.
@sourcery-ai

sourcery-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a guard and UI feedback to prevent multiple concurrent WhatsApp connection requests when the QR or pairing code buttons are clicked repeatedly.

Sequence diagram for guarded WhatsApp connection requests

sequenceDiagram
  actor User
  participant DashboardInstance
  participant Server

  User->>DashboardInstance: click QRCode Button
  DashboardInstance->>DashboardInstance: handleConnect(instance.name, false)
  DashboardInstance->>DashboardInstance: [connectInFlight.current == false]
  DashboardInstance->>DashboardInstance: connectInFlight.current = true
  DashboardInstance->>DashboardInstance: setIsConnecting(true)
  DashboardInstance->>DashboardInstance: setQRCode(null)
  DashboardInstance->>Server: GET /instance/connect
  Server-->>DashboardInstance: qrCode or pairingCode
  DashboardInstance->>DashboardInstance: setQRCode / setPairingCode
  DashboardInstance->>DashboardInstance: connectInFlight.current = false
  DashboardInstance->>DashboardInstance: setIsConnecting(false)

  User->>DashboardInstance: second click while request in flight
  DashboardInstance->>DashboardInstance: handleConnect(instance.name, false)
  DashboardInstance->>DashboardInstance: [connectInFlight.current == true]
  DashboardInstance-->>User: return (no-op, buttons disabled by isConnecting)
Loading

File-Level Changes

Change Details Files
Prevent multiple concurrent connection requests from repeated clicks and reflect the in-flight state in the UI.
  • Introduce an isConnecting React state flag to track when a connect request is in flight.
  • Add a connectInFlight useRef-based guard to synchronously prevent re-entrant handleConnect calls from rapid clicks.
  • Wrap connection logic in a try/catch/finally to ensure the in-flight guard and isConnecting are reset even on errors.
  • Reset QR code before each request and preserve existing pairing-code handling logic.
  • Disable both QR code and pairing code buttons based on isConnecting so users cannot issue overlapping requests.
src/pages/instance/DashboardInstance/index.tsx

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 left some high level feedback:

  • Because handleConnect is async and updates state in a finally block, consider guarding setIsConnecting with an isMounted ref or similar to avoid React warnings if the component unmounts while a request is in flight.
  • The isConnecting flag is global to the component; if in the future multiple instances can appear on this page, you may want to scope the in-flight guard and disabled state to the specific instance instead of blocking all connect buttons.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Because `handleConnect` is async and updates state in a `finally` block, consider guarding `setIsConnecting` with an `isMounted` ref or similar to avoid React warnings if the component unmounts while a request is in flight.
- The `isConnecting` flag is global to the component; if in the future multiple instances can appear on this page, you may want to scope the in-flight guard and disabled state to the specific instance instead of blocking all connect buttons.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@michumichifu

Copy link
Copy Markdown
Author

Update: this is now running in production on our own deployment, built on top of main (95d27b4) and mounted over the Manager shipped in the evoapicloud/evolution-api:2.4.0-rc2 image. No regressions, and pairing works normally with the guard in place.

Some field context on why the window is easier to hit than it looks.

The close branch of connectToWhatsapp() opens a WhatsApp connection and then waits before returning anything:

o == "close" ? (await t.connectToWhatsapp(e), await (0, Vi.delay)(2e3), t.qrCode)

So the request takes at least two seconds, and until this change the button stayed enabled for all of them. That is exactly the moment when a user wonders whether the click registered and clicks again, and the second click opens a second connection for the same instance. Each connection produces its own QR code, so the one on screen may be superseded by a socket the user cannot see.

Reported symptom from the operator side, which is what sent us looking: "every time I press generate it starts a new one, so now there are two generating".

Worth flagging for reviewers: this is deliberately a frontend guard for a backend behaviour. GET /instance/connect is a GET that opens a connection as a side effect and is not idempotent. Disabling the button fixes the common case, but the endpoint returning a fresh socket on every call is the underlying issue and may deserve its own fix in the API.

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