Skip to content

fix(baileys): clear stale credentials when a 401 closes the initial connection - #2680

Open
michumichifu wants to merge 1 commit into
evolution-foundation:developfrom
michumichifu:fix/clear-stale-creds-on-logged-out
Open

fix(baileys): clear stale credentials when a 401 closes the initial connection#2680
michumichifu wants to merge 1 commit into
evolution-foundation:developfrom
michumichifu:fix/clear-stale-creds-on-logged-out

Conversation

@michumichifu

@michumichifu michumichifu commented Aug 7, 2026

Copy link
Copy Markdown

Problem

An instance that loses its WhatsApp session becomes permanently unpairable. No amount of scanning fixes it, and the failure is reported to the user as a phone/network problem, which is why it took us a long time to trace.

When the connection closes with loggedOut before any QR code has been issued, connectionUpdate() takes this early return and the stored credentials are never touched:

const isInitialConnection = !this.instance.wuid && (this.instance.qrcode?.count ?? 0) === 0;

if (isInitialConnection) {
  this.logger.info('Initial connection closed, waiting for QR code generation...');
  return;   // <- credentials left in place
}

Those credentials are in a half-valid state: the session is gone so registered is false, but the identity survives. Here is the actual comparison from our server, between a stuck instance and a healthy one created seconds earlier:

stuck instance freshly created instance
registered false false
me.id 15108804692:5@s.whatsapp.net (none)
account present (none)
size of creds 1943 bytes 1250 bytes
GET /instance/connect {"count":0} valid QR code

Baileys then does the reasonable thing with the information it has: it tries to re-authenticate as that identity instead of requesting a pairing code. WhatsApp answers 401 because the session no longer exists, the connection closes, and we are back at the same early return.

The result is a loop roughly every ten seconds that never emits a code:

17:50:56  connecting, hasQr: false
17:50:57  close, statusCode: 401
17:50:57  "Initial connection closed, waiting for QR code generation..."
17:51:06  connecting, hasQr: false
17:51:10  close, statusCode: 401
17:51:10  "Initial connection closed, waiting for QR code generation..."

Downstream, GET /instance/connect keeps returning an empty qrCode object, so the Manager's QR dialog spins forever, and anyone scanning an older code gets "Could not log in. Check your phone's internet connection and scan the QR code again" — pointing the user at their phone rather than at the server.

Why the existing cleanup does not catch this

logoutInstance() already wipes credentials properly, and monitor.service.ts calls cleaningUp() on the logout.instance event. Neither runs here, because the early return happens before the code that emits that event.

Fix

Clear the credentials on that path, so the next attempt starts clean and can pair.

The cleanup logic already existed inside logoutInstance(); this extracts it into clearStoredCredentials() and reuses it, so both paths stay in sync rather than growing a second copy. Behaviour is unchanged for every status code other than loggedOut.

Why this matters beyond one instance

Recovering from this state currently requires stopping the container and deleting the Session row by hand — an instance that is still running rewrites the credentials from memory the moment they are deleted, which makes the obvious fix look like it does not work.

The other workaround is deleting and recreating the instance, which is what most people end up doing. That changes the instance id and token, and breaks every n8n flow, Chatwoot inbox and third-party integration pointing at it. For anyone running instances for clients, that is an expensive way out of a state the server can clear by itself.

Testing

  • tsc --noEmit: clean across the project (with the Prisma client generated).
  • eslint: clean on the changed file.
  • npm run build: succeeds.
  • The commit passed the repo's own husky / lint-staged hooks.

Diagnosed on a production deployment running 2.4.0-rc2. Manually clearing the credentials exactly as this patch does made the affected instance pair on the first scan, after an afternoon of failed attempts. A second instance on the same server still shows the same signature (connectionStatus: close with credentials present), which is what the table above compares.

Summary by Sourcery

Ensure WhatsApp Baileys instances clear rejected credentials on initial 401 closures so they can pair again instead of looping without QR codes.

Bug Fixes:

  • Clear stored auth credentials when an initial connection closes with a loggedOut/401 status before QR generation, preventing instances from becoming permanently unpairable.
  • Extract shared credential-wiping logic into a reusable helper so both logout and loggedOut connection paths consistently remove stale Baileys credentials.

…onnection

An instance that loses its session becomes permanently unpairable.

When the connection closes with loggedOut before any QR code has been issued,
connectionUpdate() takes the isInitialConnection early return and leaves the
stored credentials untouched. Those credentials are in a half-valid state: the
session is gone, so registered is false, but the identity is still there (me
and account survive).

Baileys reads that on the next attempt and does the reasonable thing with the
information it has: it tries to re-authenticate as that identity rather than
requesting a pairing code. WhatsApp answers 401 because the session no longer
exists, the connection closes, and we land back on the same early return. The
instance loops roughly every ten seconds with hasQr: false and never emits a
code, so GET /instance/connect keeps returning an empty qrCode object, the
Manager dialog spins forever, and a phone scanning any older code is told to
check its internet connection.

Recovering from this currently requires stopping the container and deleting the
Session row by hand, because an instance still running rewrites the credentials
from memory as soon as they are removed. Recreating the instance also works, but
it changes the instance id and token and breaks every n8n flow, Chatwoot inbox
and third-party integration pointing at it.

Clear the credentials on that path so the next attempt starts clean and can
pair. The cleanup already existed inside logoutInstance(); it is extracted into
clearStoredCredentials() and reused, so both paths stay in sync.
@sourcery-ai

sourcery-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors Baileys WhatsApp service credential cleanup into a reusable method and invokes it on 401/loggedOut during the initial connection path to clear half-valid credentials that otherwise make an instance permanently unpairable.

Sequence diagram for handling loggedOut during initial WhatsApp connection

sequenceDiagram
    participant BaileysClient
    participant BaileysStartupService
    participant Database
    participant Cache

    BaileysClient->>BaileysStartupService: connectionUpdate(state=close, statusCode=DisconnectReason.loggedOut)
    BaileysStartupService->>BaileysStartupService: isInitialConnection
    alt [isInitialConnection and statusCode === DisconnectReason.loggedOut]
        BaileysStartupService->>BaileysStartupService: clearStoredCredentials()
        BaileysStartupService->>Database: delete Session credentials
        BaileysStartupService->>Cache: clear auth cache
        BaileysStartupService->>BaileysStartupService: stateConnection = { state: close, statusReason: 401 }
    end
    BaileysStartupService->>BaileysStartupService: logger.info("Initial connection closed, waiting for QR code generation...")
    BaileysStartupService-->>BaileysClient: return

    Note over BaileysClient,BaileysStartupService: Next connect attempt starts without stored credentials and can request a pairing QR code
Loading

File-Level Changes

Change Details Files
Extract credential-wiping logic into a dedicated helper and reuse it for both explicit logout and initial loggedOut/401 failures.
  • Introduced a private async clearStoredCredentials() method encapsulating the logic to wipe stored auth credentials, cache entries, and mark the instance as closed.
  • Reused clearStoredCredentials() inside logoutInstance() (replacing the inline cleanup logic) to keep credential cleanup behavior centralized.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Ensure credentials are cleared when an initial connection closes with a loggedOut (401) status before any QR code is issued.
  • On initial connection close (no wuid and zero QR count), detect DisconnectReason.loggedOut and clear stored credentials instead of leaving them in a half-valid state.
  • Added a warning log when this condition occurs to aid debugging of stuck instances that loop on 401 without emitting QR codes.
  • Preserved existing behavior for all other status codes and non-initial connections, still logging and returning early after any conditional cleanup.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

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
Contributor

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


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.

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