π Bug Description
When a sender's device re-delivers the same WhatsApp message (identical key.id, new timestamp β e.g. a buggy device or an unofficial client stuck in a loop resending an auto-reply), the Chatwoot integration creates a new Chatwoot message on every arrival. There is no idempotency check on the live path.
Real-world impact (production, v2.3.7): one guest's device looped an away-message roughly once a minute. In ~25h we accumulated 837 copies of a single message in one Chatwoot conversation (and a second looping key.id later reached 539 copies). The conversation became unusable for the support team. WhatsApp mobile shows the message once (native dedup by key.id); Chatwoot shows every copy.
The interesting part: the codebase already contains exactly the right check β ChatwootImport.getExistingSourceIds(sourceIds, conversationId) queries Chatwoot's messages.source_id for WAID:<key.id> β but it is only called from the history import path (importHistoryMessages). The live path (ChatwootService.createMessage) posts to Chatwoot with source_id: WAID:<key.id> without consulting anything.
π Steps to Reproduce
- Evolution API v2.3.7 (baileys channel) + Chatwoot integration enabled.
- From a sender device, deliver the same message twice with the same
key.id (a looping unofficial client does this naturally; the events arrive with status: DELIVERY_ACK, source: 'unknown' and a fresh messageTimestamp each time).
- Watch the Chatwoot conversation.
β
Expected Behavior
Second and subsequent arrivals of an already-imported key.id for the same conversation should be ignored (the same way importHistoryMessages already filters via getExistingSourceIds), or at least be configurable to be ignored.
β Actual Behavior
Every arrival creates a new Chatwoot message with the same source_id (WAID:<key.id>). messages.source_id in Chatwoot has a non-unique index, so nothing stops the duplicates downstream either.
π‘ Suggested Fix
Call the existing check in the live path. We are running this in production (patched into createMessage) and it fully stopped the flood without affecting legitimate traffic:
// at the top of ChatwootService.createMessage(...), sourceId = "WAID:" + key.id
if (sourceId) {
try {
const existing = await chatwootImport.getExistingSourceIds([sourceId], conversationId);
if (existing.has(sourceId)) {
this.logger.warn(`[dedup] repeated message ignored source_id=${sourceId} conversation_id=${conversationId}`);
return null; // callers already handle the null ("message not sent") path
}
} catch (e) {
this.logger.warn(`[dedup] check unavailable: ${e}`); // fail open
}
}
Notes:
getExistingSourceIds already accepts the optional conversationId argument, so the lookup is cheap (indexed on source_id).
- Failing open on lookup errors keeps message delivery safe if the Chatwoot DB connection blips.
- Verified in production: 14 duplicate arrivals blocked in the first minutes across 2 looping
key.ids, zero legitimate messages affected.
π Environment
- Evolution API: v2.3.7 (image
evoapicloud/evolution-api:v2.3.7)
- Connection type: baileys
- Chatwoot: v4.16.1, integration via
CHATWOOT_ENABLED=true
- DB: PostgreSQL (shared instance for Evolution + Chatwoot import connection)
- OS: Linux (Docker)
π Bug Description
When a sender's device re-delivers the same WhatsApp message (identical
key.id, new timestamp β e.g. a buggy device or an unofficial client stuck in a loop resending an auto-reply), the Chatwoot integration creates a new Chatwoot message on every arrival. There is no idempotency check on the live path.Real-world impact (production, v2.3.7): one guest's device looped an away-message roughly once a minute. In ~25h we accumulated 837 copies of a single message in one Chatwoot conversation (and a second looping
key.idlater reached 539 copies). The conversation became unusable for the support team. WhatsApp mobile shows the message once (native dedup bykey.id); Chatwoot shows every copy.The interesting part: the codebase already contains exactly the right check β
ChatwootImport.getExistingSourceIds(sourceIds, conversationId)queries Chatwoot'smessages.source_idforWAID:<key.id>β but it is only called from the history import path (importHistoryMessages). The live path (ChatwootService.createMessage) posts to Chatwoot withsource_id: WAID:<key.id>without consulting anything.π Steps to Reproduce
key.id(a looping unofficial client does this naturally; the events arrive withstatus: DELIVERY_ACK,source: 'unknown'and a freshmessageTimestampeach time).β Expected Behavior
Second and subsequent arrivals of an already-imported
key.idfor the same conversation should be ignored (the same wayimportHistoryMessagesalready filters viagetExistingSourceIds), or at least be configurable to be ignored.β Actual Behavior
Every arrival creates a new Chatwoot message with the same
source_id(WAID:<key.id>).messages.source_idin Chatwoot has a non-unique index, so nothing stops the duplicates downstream either.π‘ Suggested Fix
Call the existing check in the live path. We are running this in production (patched into
createMessage) and it fully stopped the flood without affecting legitimate traffic:Notes:
getExistingSourceIdsalready accepts the optionalconversationIdargument, so the lookup is cheap (indexed onsource_id).key.ids, zero legitimate messages affected.π Environment
evoapicloud/evolution-api:v2.3.7)CHATWOOT_ENABLED=true