From 336d6402fec1b082b5dfd981414e574217f9e176 Mon Sep 17 00:00:00 2001 From: Diego Fornalha Date: Mon, 6 Apr 2026 21:53:57 -0300 Subject: [PATCH] fix(whatsapp): handle LoggedOut/StreamReplaced events and abort infinite reconnect The whatsapp_native event handler only handled *events.Disconnected, so permanent session invalidations (LoggedOut, StreamReplaced, TemporaryBan, ClientOutdated) fell through silently. reconnectWithBackoff() would loop forever (backoff up to 5 min) on a dead session, leaving the gateway stuck and unresponsive. - Add loggedOut atomic flag set on terminal events - Handle LoggedOut, StreamReplaced, ConnectFailure, KeepAliveTimeout, TemporaryBan, and ClientOutdated with proper logging - Abort reconnect loop immediately when session is permanently invalid - Reset loggedOut on Start() so re-pairing works after restart Co-Authored-By: Claude Opus 4.6 (1M context) --- .../whatsapp_native/whatsapp_native.go | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/pkg/channels/whatsapp_native/whatsapp_native.go b/pkg/channels/whatsapp_native/whatsapp_native.go index d0a74a405..32f56b9bb 100644 --- a/pkg/channels/whatsapp_native/whatsapp_native.go +++ b/pkg/channels/whatsapp_native/whatsapp_native.go @@ -58,6 +58,7 @@ type WhatsAppNativeChannel struct { reconnectMu sync.Mutex reconnecting bool stopping atomic.Bool // set once Stop begins; prevents new wg.Add calls + loggedOut atomic.Bool // set when session is permanently invalid (LoggedOut, StreamReplaced, etc.) wg sync.WaitGroup // tracks background goroutines (QR handler, reconnect) } @@ -88,6 +89,7 @@ func (c *WhatsAppNativeChannel) Start(ctx context.Context) error { // and Stop() which coordinate under the same lock. c.reconnectMu.Lock() c.stopping.Store(false) + c.loggedOut.Store(false) c.reconnecting = false c.reconnectMu.Unlock() @@ -267,9 +269,37 @@ func (c *WhatsAppNativeChannel) Stop(ctx context.Context) error { } func (c *WhatsAppNativeChannel) eventHandler(evt any) { - switch evt.(type) { + switch v := evt.(type) { case *events.Message: - c.handleIncoming(evt.(*events.Message)) + c.handleIncoming(v) + case *events.LoggedOut: + c.loggedOut.Store(true) + logger.ErrorCF("whatsapp", "WhatsApp session logged out — re-pairing required", map[string]any{ + "on_connect": v.OnConnect, + "reason": v.Reason.String(), + }) + case *events.StreamReplaced: + c.loggedOut.Store(true) + logger.ErrorCF("whatsapp", "WhatsApp stream replaced by another session — re-pairing required", nil) + case *events.KeepAliveTimeout: + logger.WarnCF("whatsapp", "WhatsApp keep-alive timeout", map[string]any{ + "error_count": v.ErrorCount, + "last_success": v.LastSuccess.Format(time.RFC3339), + }) + case *events.ConnectFailure: + logger.ErrorCF("whatsapp", "WhatsApp connect failure", map[string]any{ + "reason": v.Reason.String(), + "message": v.Message, + }) + case *events.TemporaryBan: + c.loggedOut.Store(true) + logger.ErrorCF("whatsapp", "WhatsApp temporary ban — stopping reconnect", map[string]any{ + "code": v.Code.String(), + "expire": v.Expire.String(), + }) + case *events.ClientOutdated: + c.loggedOut.Store(true) + logger.ErrorCF("whatsapp", "WhatsApp client outdated — update whatsmeow required", nil) case *events.Disconnected: logger.InfoCF("whatsapp", "WhatsApp disconnected, will attempt reconnection", nil) c.reconnectMu.Lock() @@ -303,6 +333,11 @@ func (c *WhatsAppNativeChannel) reconnectWithBackoff() { backoff := reconnectInitial for { + if c.loggedOut.Load() { + logger.ErrorCF("whatsapp", "Aborting reconnect — session invalid, re-pairing required", nil) + return + } + select { case <-c.runCtx.Done(): return