From 5a9d5b399c7c6df6f0c162919e338136cca95c9e Mon Sep 17 00:00:00 2001 From: octo-patch Date: Mon, 20 Apr 2026 11:49:06 +0800 Subject: [PATCH 1/3] fix(baileys): prevent healthy instances from being killed after stream:error 515 When WhatsApp sends stream:error code=515 (Connection Replaced), Baileys handles the reconnect correctly and fires connection.update with state='open'. However, WhatsApp then sends a 401 (loggedOut) to clean up the old session slot, which Evolution API incorrectly treated as a real logout, killing the newly-connected healthy instance. The fix tracks when a stream:error 515 node arrives via the CB:stream:error WebSocket event. If a loggedOut (401) close event fires within 30 seconds of a 515, it is treated as a transient reconnect rather than a real logout. Fixes #2498 --- .../channel/whatsapp/whatsapp.baileys.service.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index df5e3add5..9197799a4 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -264,6 +264,7 @@ export class BaileysStartupService extends ChannelStartupService { private isDeleting = false; // Flag to prevent reconnection during deletion private logBaileys = this.configService.get('LOG').BAILEYS; private eventProcessingQueue: Promise = Promise.resolve(); + private _lastStream515At = 0; // Cumulative history sync counters (reset on new sync or completion) private historySyncMessageCount = 0; @@ -506,7 +507,12 @@ export class BaileysStartupService extends ChannelStartupService { return; } - const shouldReconnect = !codesToNotReconnect.includes(statusCode); + // If a stream:error 515 (Baileys' "restart needed" handshake) just fired, + // a follow-up loggedOut is the expected restart signal — not an actual + // logout — so reconnect anyway. + const recentStream515 = Date.now() - this._lastStream515At < 30_000; + const shouldReconnect = + !codesToNotReconnect.includes(statusCode) || (statusCode === DisconnectReason.loggedOut && recentStream515); this.logger.info({ message: 'Connection closed, evaluating reconnection', @@ -515,6 +521,7 @@ export class BaileysStartupService extends ChannelStartupService { instanceName: this.instance.name, }); + if (shouldReconnect) { // Add 3 second delay before reconnection to prevent rapid reconnection loops this.logger.info('Reconnecting in 3 seconds...'); @@ -813,6 +820,12 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.CALL, payload, true, ['websocket']); }); + this.client.ws.on('CB:stream:error', (node: any) => { + if (node?.attrs?.code === '515') { + this._lastStream515At = Date.now(); + } + }); + this.phoneNumber = number; return this.client; From ce314eb568f3727edb5ba027d61193ea47ab0bf9 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Mon, 20 Apr 2026 13:13:08 +0800 Subject: [PATCH 2/3] fix(baileys): name the 515 reconnect grace + tighten stream-error types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses sourcery-ai review feedback on the previous commit: - Extract the 30 000ms reconnect grace window into a named class constant STREAM_515_RECONNECT_GRACE_MS so future tuning is self-documenting rather than a literal scattered through the close handler. - Extract the magic '515' string into STREAM_ERROR_CODE_RECONNECT. - Replace the loose 'node: any' on the 'CB:stream:error' handler with a minimal structural type ({ attrs?: { code?: string | number } }) so the payload shape is documented and type-checked. - Compare the code via String(...) so a numeric 515 from the underlying socket library still triggers the grace window — the original literal '515' check would have silently broken on a type change. --- .../channel/whatsapp/whatsapp.baileys.service.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 9197799a4..81ba42e25 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -276,6 +276,13 @@ export class BaileysStartupService extends ChannelStartupService { private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing private readonly UPDATE_CACHE_TTL_SECONDS = 30 * 60; // 30 minutes - avoid duplicate status updates + // Reconnect behaviour for the Baileys "stream:error 515" sequence. + // After WhatsApp emits 515 it usually closes with `loggedOut`; that close is *not* a real logout + // and we should reconnect. We treat any close arriving within this grace window as 515-driven. + private static readonly STREAM_515_RECONNECT_GRACE_MS = 30_000; + // The numeric WhatsApp stream-error code that triggers the grace-period reconnect above. + private static readonly STREAM_ERROR_CODE_RECONNECT = '515'; + public stateConnection: wa.StateConnection = { state: 'close' }; public phoneNumber: string; @@ -510,7 +517,8 @@ export class BaileysStartupService extends ChannelStartupService { // If a stream:error 515 (Baileys' "restart needed" handshake) just fired, // a follow-up loggedOut is the expected restart signal — not an actual // logout — so reconnect anyway. - const recentStream515 = Date.now() - this._lastStream515At < 30_000; + const recentStream515 = + Date.now() - this._lastStream515At < BaileysStartupService.STREAM_515_RECONNECT_GRACE_MS; const shouldReconnect = !codesToNotReconnect.includes(statusCode) || (statusCode === DisconnectReason.loggedOut && recentStream515); @@ -820,8 +828,8 @@ export class BaileysStartupService extends ChannelStartupService { this.sendDataWebhook(Events.CALL, payload, true, ['websocket']); }); - this.client.ws.on('CB:stream:error', (node: any) => { - if (node?.attrs?.code === '515') { + this.client.ws.on('CB:stream:error', (node: { attrs?: { code?: string | number } }) => { + if (String(node?.attrs?.code) === BaileysStartupService.STREAM_ERROR_CODE_RECONNECT) { this._lastStream515At = Date.now(); } }); From 1118470091315e1f310a84665f5ce3c28ba973e9 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Thu, 7 May 2026 13:10:46 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(baileys):=20satisfy=20prettier=20?= =?UTF-8?q?=E2=80=94=20collapse=20515=20reconnect=20guard=20onto=20one=20l?= =?UTF-8?q?ine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Check Code Quality lint failed on prettier/prettier (the recentStream515 expression fits within the project's 120-col printWidth on a single line, while shouldReconnect still needs to break across two). Co-authored-by: Octopus --- .../integrations/channel/whatsapp/whatsapp.baileys.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 81ba42e25..977d9d3ec 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -517,8 +517,7 @@ export class BaileysStartupService extends ChannelStartupService { // If a stream:error 515 (Baileys' "restart needed" handshake) just fired, // a follow-up loggedOut is the expected restart signal — not an actual // logout — so reconnect anyway. - const recentStream515 = - Date.now() - this._lastStream515At < BaileysStartupService.STREAM_515_RECONNECT_GRACE_MS; + const recentStream515 = Date.now() - this._lastStream515At < BaileysStartupService.STREAM_515_RECONNECT_GRACE_MS; const shouldReconnect = !codesToNotReconnect.includes(statusCode) || (statusCode === DisconnectReason.loggedOut && recentStream515);