Skip to content

fix: do not resend an empty message when a retry receipt cannot be answered - #2728

Open
iagovelasco3 wants to merge 1 commit into
evolution-foundation:developfrom
iagovelasco3:fix/retry-getmessage-empty-conversation
Open

iagovelasco3 wants to merge 1 commit into
evolution-foundation:developfrom
iagovelasco3:fix/retry-getmessage-empty-conversation

Conversation

@iagovelasco3

@iagovelasco3 iagovelasco3 commented Sep 14, 2026

Copy link
Copy Markdown

📋 Description

getMessage returns { conversation: '' } when the message row is not found or the lookup throws.

Baileys only skips a retry resend when getMessage resolves to a falsy value (sendMessagesAgain in Socket/messages-recv, if (msg && await willSendMessageAgain(...))). Returning an object makes it relay an empty message reusing the original message id, so every member of the chat receives a bubble with a timestamp and no content. It repeats on each retry attempt, up to maxMsgRetryCount per requesting participant.

This is easy to hit in groups: any member that fails to decrypt (No matching sessions found, Received message with old counter) asks for a retry, and by then the original message may no longer be in the Message table (retention, instance recreated, or a message this instance never persisted).

This PR returns undefined instead, in both the not-found and the error path. Baileys then logs recv retry request, but message not available and sends nothing, which is the correct behavior.

Two smaller changes in the same method:

  • webMessageInfo[0] is now optional-chained, so an empty result set no longer throws on .message and falls through to the catch.
  • the poll branch reuses the local message variable instead of re-reading webMessageInfo[0].message three times.

The other consumers of getMessage in Baileys (poll vote decrypt and event response decrypt in process-message) already guard with if (msg), so they also behave correctly with undefined. Today they receive the truthy { conversation: '' }, proceed, and fail later on a missing messageSecret.

🔗 Related Issue

No open issue; found while investigating empty bubbles reported by end users.

🧪 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 in production on a deployment of 11 Evolution instances (~480 connected sessions, group-heavy traffic).

Before: ~17,700 empty bubbles persisted in 24h across the deployment, concentrated in the busiest group senders. Traced by finding the same message id re-arriving with {"conversation": ""} after having arrived earlier with real content, for example an imageMessage sent at 12:21 UTC that came back at 12:38 and 12:39 as an empty conversation. 619 of 623 empty ids in a 24h sample never existed with content anywhere in the database.

After: 10 of the 11 instances went to exactly zero empty messages with fromMe, holding over a 40 minute window with ~3,000 retries answered with undefined in that period. The remaining instance only receives them, from a third-party sender outside the deployment. Real message delivery was unchanged: the two campaign accounts under investigation sent 276 and 240 messages after the change with no empty ones and no new errors.

tsc --noEmit, eslint and prettier --check are clean on the changed file.

📝 Additional Notes

The same defect is present on main. I targeted develop per CONTRIBUTING.md; happy to open a second PR against main if you prefer.

The trade-off is explicit: a recipient that asks for a retry of a message we no longer have will not receive that message. That is already the outcome today, except the recipient also gets a confusing empty bubble.

🤖 Generated with Claude Code

Summary by Sourcery

Skip WhatsApp retry resends when the original message cannot be retrieved instead of sending empty message bubbles.

Bug Fixes:

  • Prevent retry requests for unavailable WhatsApp messages from relaying empty message bubbles with reused message IDs.
  • Return no message when message lookup fails or finds no record, allowing retry handling to skip the resend.

Enhancements:

  • Make message retrieval safer for empty lookup results while preserving poll message handling.

…swered

`getMessage` returned `{ conversation: '' }` whenever the message row was not
found or the lookup threw. Baileys only skips a retry resend when `getMessage`
resolves to a falsy value, so returning that object makes `sendMessagesAgain`
relay an EMPTY message reusing the original message id. Group members then see
a bubble with a timestamp and no content, repeated on every retry attempt
(maxMsgRetryCount per requesting participant).

It is easy to hit in groups: any member that fails to decrypt asks for a retry,
and by then the original message may be gone from the DB (retention, instance
recreated, or a message this instance never persisted).

Measured on a 11-cell deployment before the fix: ~17.7k empty bubbles in 24h.
After returning `undefined` instead, 10 of 11 cells dropped to zero, with the
remainder coming from a third-party sender outside the deployment. No change in
delivery of real messages.

Also guards `webMessageInfo[0]` with optional chaining, so an empty result set no
longer throws on `.message`, and reuses the local `message` variable in the poll
branch. The poll and event decrypt callers in Baileys already guard with
`if (msg)`, so they behave correctly with `undefined` too.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@sourcery-ai

sourcery-ai Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Fixes retry handling so unavailable messages are skipped instead of being resent as empty bubbles, while retaining existing poll handling and improving lookup safety and diagnostics.

Sequence diagram for skipping unavailable WhatsApp retry messages

sequenceDiagram
    participant Baileys
    participant GetMessage as getMessage
    participant MessageDB as Message lookup
    participant Logger
    participant Chat as Chat recipients

    Baileys->>GetMessage: getMessage(key, full)
    GetMessage->>MessageDB: find message by key
    MessageDB-->>GetMessage: message or empty/error
    alt message found
        GetMessage-->>Baileys: message
        Baileys->>Chat: resend original message
    else message unavailable or lookup fails
        GetMessage->>Logger: debug unavailable message
        GetMessage-->>Baileys: undefined
        Baileys-->>Chat: skip retry resend
    end
Loading

Flow diagram for safe retry message lookup

flowchart TD
    A[getMessage] --> B[Lookup message]
    B --> C{message exists?}
    C -->|yes| D{pollCreationMessage?}
    D -->|yes| E[Restore messageSecret and return poll message]
    D -->|no| F[Return message]
    C -->|no or lookup error| G[Log diagnostic]
    G --> H[Return undefined]
    H --> I[Baileys skips resend]
Loading

File-Level Changes

Change Details Files
Return a falsy value when the requested message cannot be retrieved, preventing Baileys from relaying an empty message under the original message ID.
  • Return undefined for missing database rows and lookup errors.
  • Add debug logging for unavailable messages and failed lookups.
  • Preserve normal message retrieval and poll-specific reconstruction behavior.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Make message access safer and reduce redundant lookups in the retry-message path.
  • Use optional chaining when accessing the first lookup result.
  • Reuse a local message variable for poll and normal message handling.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

Possibly linked issues

  • #N/A: The PR directly fixes the issue by returning undefined for missing or failed lookups, preventing empty-message retry relays.

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 assessment

Approved.


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

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