Skip to content

fix: conecta ao WhatsApp corretamente (client outdated + vazamento de conexao + corrida no pareamento) - #199

Open
intelektos wants to merge 3 commits into
evolution-foundation:developfrom
intelektos:fix/whatsmeow-outdated-client
Open

intelektos wants to merge 3 commits into
evolution-foundation:developfrom
intelektos:fix/whatsmeow-outdated-client

Conversation

@intelektos

@intelektos intelektos commented Sep 15, 2026

Copy link
Copy Markdown

Contexto

Em produção, instâncias novas do evolution-go paravam de conseguir conectar ao WhatsApp com Client outdated (405) connect failure, e nenhum QR code chegava a ser gerado com sucesso. Investigando a fundo, encontrei três problemas distintos e independentes na mesma cadeia de conexão.

Depende de evolution-foundation/whatsmeow#5 (atualização do fork vendorizado).

Os três fixes

1. sqlstore.Container recriado a cada StartClient (vazamento de conexão)

StartClient chamava sqlstore.New(...) a cada execução e nunca fechava o container resultante — cada chamada vazava um pool inteiro de conexões database/sql ao Postgres. Uma instância não pareada, que reinicia sozinha no loop de QR, esgotava o max_connections do banco em minutos (medido em produção: ~2 conexões/minuto, ~35 min até esgotar 100 conexões — e o Postgres é compartilhado com o resto do stack, então a exaustão não fica contida neste serviço).

Correção: um único sqlstore.Container por processo, criado via sync.Once.

2. Versão do WhatsApp Web buscada mas nunca aplicada ao handshake

O código buscava corretamente a versão mais recente do WhatsApp Web (fetchWhatsAppWebVersion) ou recebia uma via WHATSAPP_VERSION_*, mas só escrevia o valor em store.DeviceProps.Version (metadados do device anunciados no pairing) — nunca em store.waVersion via store.SetWAVersion, que é o campo usado de fato no handshake. Resultado: o connect sempre negociava com a versão compilada na lib, até o WhatsApp recusar com 405.

Isoladamente esse fix não bastou — o fork vendorizado do whatsmeow estava 126 commits (~5 meses) atrás do upstream, e o protocolo mudou no período, não só o número de versão. Ver PR companheira no repositório da lib.

3. Corrida entre pareamento e o loop de QR (causa do "the store doesn't contain a device JID")

No loop for evt := range qrChan, ao receber evt.Event == "success" o código apenas logava e continuava consumindo o canal em vez de sair. Se um evento code tardio chegasse depois do success (o canal só fecha quando o whatsmeow decide, não no instante exato do PairSuccess), o contador de QR code continuava incrementando e podia disparar Logout() por "Maximum QR code count reached" sobre uma sessão que tinha acabado de parear com sucesso.

Reproduzido em produção:

09:50:41  QR Pair Success ... JID '553498166469:4@s.whatsapp.net'
09:51:59  Error sending message: the store doesn't contain a device JID
09:52:13  Error sending message: the store doesn't contain a device JID
09:52:24  Maximum QR code count reached, forcing logout
09:52:40  Starting new client instance (reidrata corretamente)

Correção: break ao receber success, parando de consumir o canal.

Outras mudanças de suporte

  • go.mod/go.sum atualizados via go mod tidy após o merge do whatsmeow-lib, que agora exige go >= 1.26.0.
  • Dockerfile: imagem base golang:1.25.0-alpinegolang:1.27.1-alpine.
  • pkg/user/service/user_service.go: SetStatusMessage passou a receber types.SetStatusInput em vez de string (API nova do upstream, suporta emoji e duração de status); adaptado o único call-site.

Validação

go build ./... e go vet ./... limpos no módulo completo (golang:1.27.1-alpine).

Testado em produção em dois servidores: pareamento via QR, envio de mensagem imediatamente após o pareamento (a janela onde o bug #3 se manifestava) e recebimento de resposta do destinatário, todos confirmados funcionando.

🤖 Generated with Claude Code

Summary by Sourcery

Restabeleça conexões confiáveis com o WhatsApp, evitando esgotamento de conexões, rejeição por cliente desatualizado e logout indevido após o pareamento.

Bug Fixes:

  • Corrija as falhas de conexão com o WhatsApp causadas por versões de handshake desatualizadas, vazamento de conexões e corrida durante o pareamento por QR code.

Enhancements:

  • Reutilize um único container de armazenamento por processo e aplique corretamente a versão atual do WhatsApp Web ao handshake.
  • Atualize a integração de status do usuário para a nova API do whatsmeow.

Build:

  • Atualize a versão mínima do Go e a imagem de build do Docker, junto com as dependências do módulo.

Tests:

  • Valide o módulo completo com build e vet, além de confirmar em produção o pareamento, envio e recebimento de mensagens.

Chores:

  • Atualize o whatsmeow-lib vendorizado para a versão compatível com o protocolo atual do WhatsApp.

intelektosdev and others added 3 commits September 12, 2026 21:10
… chamada

StartClient criava um sqlstore.Container a cada chamada e nunca o fechava.
Cada container abre o proprio pool database/sql, entao toda chamada vazava
um pool inteiro de conexoes ao Postgres.

O vazamento nao depende de interacao do usuario. Uma instancia que nunca foi
pareada entra em laco sozinha:

  QR evolution-foundation#1..evolution-foundation#5 -> "Maximum QR code count reached" -> QRTimeout -> LoggedOut
            -> "Restarting client" -> StartClient -> NOVO container
            -> QR evolution-foundation#1..evolution-foundation#5 -> ...

Medido em producao com uma unica instancia nao pareada e ninguem usando a UI:
27 ciclos de restart em uma hora, ~2 conexoes por minuto. O max_connections
padrao (100) esgotou em ~35 minutos, e a partir dai:

  [ERR] Failed to create container: failed to upgrade database:
        failed to check if version table is up to date: pq: sorry, too many
        clients already
  [GIN] 400 | GET "/instance/qr"

Como o Postgres e compartilhado com o restante da stack, a exaustao nao ficou
contida neste servico: no ponto de falha nem o psql conseguia conectar.

O sqlstore.Container e seguro para uso concorrente e ja faz pooling interno,
entao uma unica instancia atende todos os clientes pelo tempo de vida do
processo. Isso tambem evita reexecutar a checagem de migracao de schema a cada
StartClient, que e justamente o passo que falha na mensagem acima.

O container fica em escopo de pacote, guardado por sync.Once, porque
StartClient tem receiver por valor (w whatsmeowService) e um campo na struct
nao persistiria entre chamadas.

Comportamento preservado: escolha entre Postgres e SQLite, logger condicionado
a WaDebug, mesma DSN do SQLite e mesmo tratamento de erro.

Verificado com go build e go vet no pacote alterado (Go 1.25.0-alpine).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O connect falhava com "Client outdated (405)" mesmo apos o codigo
buscar corretamente a versao mais recente do WhatsApp Web
(fetchWhatsAppWebVersion) ou receber uma via WHATSAPP_VERSION_*: o valor
obtido era escrito apenas em store.DeviceProps.Version (metadados do
device anunciados no pairing) e nunca em store.waVersion, que e o
campo efetivamente usado no handshake via store.SetWAVersion.

Verificado em producao: a versao baixada (2.3000.1047557390) nunca
chegava ao connect, que continuava negociando com a versao compilada
(2.3000.1035920091), ate o WhatsApp recusar com 405 e nenhum QR code
ser gerado.

applyWAVersion() chama store.SetWAVersion nos dois ramos de resolucao
de versao (env vars e fetch do WhatsApp Web), protegida por mutex por
waVersion ser global no pacote store, sem sincronizacao propria, e
StartClient rodar em goroutine por instancia.

Isoladamente essa correcao nao bastou: o handshake continuou recusando
mesmo com a versao correta, porque o whatsmeow-lib vendorizado estava
126 commits atras do upstream (5 meses) e o protocolo mudou, nao so o
numero de versao. Ver commit de merge do submodulo whatsmeow-lib.

Mudancas de suporte:
- go.mod/go.sum: atualizados via go mod tidy apos o merge do
  whatsmeow-lib, que agora exige go >= 1.26.0.
- Dockerfile: imagem base golang:1.25.0-alpine -> golang:1.27.1-alpine.
- pkg/user/service/user_service.go: SetStatusMessage passou a receber
  types.SetStatusInput em vez de string (API nova do upstream, suporta
  emoji e duracao de status); adaptado o unico call-site.

Validado com go build e go vet no modulo completo (golang:1.27.1-alpine).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ao receber evt.Event == "success" no loop de QR, o codigo apenas
logava "QR pairing ok!" e continuava no for evt := range qrChan em vez
de sair. Isso deixava uma janela de corrida: se um evento "code" tardio
chegasse depois do "success" (o canal so fecha quando o whatsmeow
decide fecha-lo, nao no instante exato do PairSuccess), mycli.qrcodeCount
era incrementado, e ao atingir QrcodeMaxCount o servico forcava
Logout() sobre uma sessao que tinha acabado de parear com sucesso.

Reproduzido em producao logo apos a atualizacao do whatsmeow-lib:

  09:50:41  QR Pair Success ... JID '553498166469:4@s.whatsapp.net'
  09:51:59  Error sending message: the store doesn't contain a device JID
  09:52:13  Error sending message: the store doesn't contain a device JID
  09:52:24  Maximum QR code count reached, forcing logout
  09:52:40  Starting new client instance (reidrata corretamente)

Entre o pareamento (09:50:41) e o logout forcado (09:52:24), qualquer
tentativa de enviar mensagem falhava com esse erro porque o client em
memoria tinha side de logout em andamento sobre um Store que acabara
de ganhar Store.ID. Apos o proximo "Starting new client instance" o
device era reidratado do banco e o envio voltava a funcionar - por
isso o efeito era transitorio e dificil de reproduzir sob demanda.

O break interrompe o consumo do canal no evento de sucesso, entao
eventos "code" que cheguem depois (se o whatsmeow ainda nao fechou o
canal) nao incrementam mais o contador nem competem com o restante do
fluxo de conexao ja pareada.

Validado com go build e go vet no modulo completo (golang:1.27.1-alpine).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Sep 15, 2026

Copy link
Copy Markdown

Reviewer's Guide

A implementação corrige a cadeia de conexão ao WhatsApp ao reutilizar o container de armazenamento, aplicar a versão real do Web WhatsApp no handshake e encerrar o fluxo de QR após o pareamento; também incorpora a atualização do whatsmeow e os ajustes necessários de compatibilidade do Go e das APIs.

Sequence diagram for the corrected WhatsApp client startup

sequenceDiagram
    participant StartClient
    participant StoreContainer
    participant WhatsAppVersion
    participant Whatsmeow
    participant QRChannel

    StartClient->>StoreContainer: storeContainer()
    StoreContainer-->>StartClient: shared sqlstore.Container
    StartClient->>WhatsAppVersion: fetchWhatsAppWebVersion()
    WhatsAppVersion-->>StartClient: clientVersion
    StartClient->>Whatsmeow: applyWAVersion(version)
    StartClient->>Whatsmeow: Connect()
    Whatsmeow-->>QRChannel: code
    QRChannel-->>StartClient: code
    Whatsmeow-->>QRChannel: success
    QRChannel-->>StartClient: success
    StartClient->>QRChannel: break
Loading

File-Level Changes

Change Details Files
Centraliza o armazenamento SQL e evita o vazamento de pools durante reinícios do cliente.
  • Inicializa um único sqlstore.Container por processo com sync.Once.
  • Mantém a seleção de PostgreSQL/SQLite, DSN e logging na criação compartilhada.
  • Propaga erros de inicialização do container para o fluxo de StartClient.
pkg/whatsmeow/service/whatsmeow.go
Aplica efetivamente a versão atual do WhatsApp Web durante a negociação e atualiza a base vendorizada do protocolo.
  • Adiciona aplicação sincronizada da versão via store.SetWAVersion.
  • Aplica a versão tanto da configuração WHATSAPP_VERSION_* quanto da consulta ao WhatsApp Web.
  • Atualiza a dependência whatsmeow-lib e seus módulos transitivos conforme o upstream.
pkg/whatsmeow/service/whatsmeow.go
whatsmeow-lib
go.mod
go.sum
Elimina a corrida entre o sucesso do pareamento e eventos tardios do fluxo de QR.
  • Interrompe o consumo de qrChan imediatamente após o evento success, evitando contagem adicional de QR e logout indevido.
pkg/whatsmeow/service/whatsmeow.go
Adapta o código às novas exigências da API e do toolchain do whatsmeow atualizado.
  • Passa SetStatusInput ao definir o status, preservando o texto no novo contrato da API.
  • Atualiza a versão mínima do Go e a imagem de build do Docker.
pkg/user/service/user_service.go
go.mod
Dockerfile

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Fixed security issues:

  • golang.org/x/crypto (link)
  • golang.org/x/net (link)
  • golang.org/x/text (link)
Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="pkg/whatsmeow/service/whatsmeow.go" line_range="311-337" />
<code_context>
+var (
</code_context>
<issue_to_address>
**issue (bug_risk):** `sync.Once` permanently caches the first `sqlstore.New` error in `sharedStoreContainerErr`; if the database is temporarily unavailable during the first `StartClient`, every later client start returns the same error without retrying container creation, even after the database recovers.

**Triggers:** When the first call to `storeContainer` fails because Postgres/SQLite is temporarily unavailable or migration initialization fails.

**Suggested fix:** Use retryable initialization instead of poisoning the process-wide singleton after the first failed creation, or reset the initialization state after an error.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and this changes WhatsApp authentication and pairing behavior, including the global client version used during handshakes and the point at which QR events stop being processed; a defect could disconnect sessions, prevent pairing, or trigger incorrect logout behavior. Reverting stops the new behavior, but sessions already logged out or pairings that failed may require re-pairing, and the shared store can cause a process-wide connection or database outage before that revert.

Blocking findings: pkg/whatsmeow/service/whatsmeow.go:337


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

Comment on lines +311 to +337
var (
sharedStoreContainer *sqlstore.Container
sharedStoreContainerErr error
sharedStoreContainerOnce sync.Once
)

func (w whatsmeowService) storeContainer() (*sqlstore.Container, error) {
sharedStoreContainerOnce.Do(func() {
var dbLog waLog.Logger
if w.config.WaDebug != "" {
dbLog = waLog.Stdout("Database", w.config.WaDebug, true)
}

if w.config.PostgresAuthDB != "" {
sharedStoreContainer, sharedStoreContainerErr = sqlstore.New(
context.Background(), "postgres", w.config.PostgresAuthDB, dbLog,
)
return
}

dsn := fmt.Sprintf("file:%s/dbdata/main.db?_pragma=foreign_keys(1)&_busy_timeout=5000&cache=shared&mode=rwc&_journal_mode=WAL", w.exPath)
sharedStoreContainer, sharedStoreContainerErr = sqlstore.New(
context.Background(), "sqlite", dsn, dbLog,
)
})

return sharedStoreContainer, sharedStoreContainerErr

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): sync.Once permanently caches the first sqlstore.New error in sharedStoreContainerErr; if the database is temporarily unavailable during the first StartClient, every later client start returns the same error without retrying container creation, even after the database recovers.

Triggers: When the first call to storeContainer fails because Postgres/SQLite is temporarily unavailable or migration initialization fails.

Suggested fix: Use retryable initialization instead of poisoning the process-wide singleton after the first failed creation, or reset the initialization state after an error.

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.

3 participants