fix(chatwoot): skip duplicate messages when a device redelivers the same key.id - #2716
fix(chatwoot): skip duplicate messages when a device redelivers the same key.id#2716vin1i wants to merge 1 commit into
Conversation
…y.id When a sender device redelivers a message with the same key.id (for example an unofficial client stuck in a resend loop), the live event path posted a new Chatwoot message on every arrival; one production report accumulated 837 copies of a single message. Reuse the dedup check that sendData already applies to media messages: query Chatwoot's messages.source_id through getExistingSourceIds before posting, gated by isImportHistoryAvailable, and skip the message when the WAID source id already exists in the target conversation. The helper is fail open: on lookup errors it logs and returns an empty set, so delivery is never blocked. The check sits at the top of the messages.upsert/send.message block so it covers text, reactions, interactive buttons, ads and media, and does not touch the messages.edit path, which legitimately re-posts using the original message's source id. Fixes evolution-foundation#2675
Reviewer's guide (collapsed on small PRs)Reviewer's GuideThe live messages.upsert/send.message flow now performs a single Chatwoot source-ID lookup immediately after resolving the conversation, preventing duplicate key.id deliveries across text, reactions, interactive, ads, and media while preserving edits, multi-message interactive events, and fail-open delivery behavior. Sequence diagram for live message deduplicationsequenceDiagram
participant WA as WhatsApp
participant CS as ChatwootService
participant CI as chatwootImport
participant CW as Chatwoot
WA->>CS: messages.upsert/send.message
CS->>CS: isImportHistoryAvailable()
alt import history available and key.id exists
CS->>CI: getExistingSourceIds([WAID:key.id], getConversation)
alt source id already exists
CI-->>CS: existing source id
CS->>CS: logger.warn(Message already saved on chatwoot)
CS-->>WA: skip duplicate
else source id not found
CI-->>CS: empty set
CS->>CW: create live message
end
else import history unavailable or key.id absent
CS->>CW: create live message
end
alt lookup error
CI-->>CS: empty set
CS->>CW: create live message
end
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts" line_range="2243-2245" />
<code_context>
return;
}
+ if (body.key.id && this.isImportHistoryAvailable()) {
+ const sourceId = 'WAID:' + body.key.id;
+ const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], getConversation);
+ if (messageAlreadySaved) {
+ if (messageAlreadySaved.size > 0) {
</code_context>
<issue_to_address>
**issue (bug_risk):** The deduplication is a non-atomic read-then-post sequence: two redelivered events for the same `key.id` can both query before either Chatwoot message is created, both receive an empty set, and both post duplicate messages.
**Triggers:** When duplicate deliveries for the same key.id are processed concurrently.
**Suggested fix:** Serialize deduplication per source ID or enforce a unique Chatwoot/database constraint so the lookup and insertion cannot succeed concurrently.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 1 finding to address first, and if the source-ID lookup is wrong or a key is reused, a legitimate message can be skipped, or a duplicate can already be persisted in Chatwoot before reverting stops the behavior. Reverting does not restore a skipped message or remove duplicates that were already sent.
Blocking findings: src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts:2245
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| if (body.key.id && this.isImportHistoryAvailable()) { | ||
| const sourceId = 'WAID:' + body.key.id; | ||
| const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], getConversation); |
There was a problem hiding this comment.
issue (bug_risk): The deduplication is a non-atomic read-then-post sequence: two redelivered events for the same key.id can both query before either Chatwoot message is created, both receive an empty set, and both post duplicate messages.
Triggers: When duplicate deliveries for the same key.id are processed concurrently.
Suggested fix: Serialize deduplication per source ID or enforce a unique Chatwoot/database constraint so the lookup and insertion cannot succeed concurrently.
Fixes #2675
Root cause
When a sender device redelivers a message with the same key.id (for example an unofficial client stuck in a resend loop), the live event path (messages.upsert / send.message) posts a new Chatwoot message on every arrival. There is no idempotency check on that path: the reporter accumulated 837 copies of a single message in one conversation, while WhatsApp itself shows it once thanks to native key.id dedup.
Fix
Reuse the exact dedup pattern sendData already applies to media messages: query Chatwoot's messages.source_id via chatwootImport.getExistingSourceIds before posting, gated by isImportHistoryAvailable(), and skip the message when WAID:<key.id> already exists in the target conversation. The check runs once at the top of the messages.upsert / send.message block, right after the conversation is resolved, so a single block covers every live path: plain text (group and direct), reactions, interactive button messages, ads and media.
Design decisions
Verification
The repository has no unit test infrastructure (npm test points to a non-existent test/all.test.ts), so no automated test is included. npm run lint:check and npm run build pass. Behavior per scenario:
Known limitations (shared with the existing sendData check)
Note: open PR #2614 changes the second argument of getExistingSourceIds from a number to an options object; whichever lands second needs a one-line adjustment in this call.
Summary by Sourcery
Skip live messages whose WhatsApp key IDs are already recorded in the target Chatwoot conversation.
Bug Fixes:
Enhancements: