fix: conecta ao WhatsApp corretamente (client outdated + vazamento de conexao + corrida no pareamento) - #199
Conversation
… 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>
Reviewer's GuideA 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 startupsequenceDiagram
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
File-Level Changes
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
Fixed security issues:
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
| 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 |
There was a problem hiding this comment.
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.
Contexto
Em produção, instâncias novas do
evolution-goparavam de conseguir conectar ao WhatsApp comClient 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.Containerrecriado a cadaStartClient(vazamento de conexão)StartClientchamavasqlstore.New(...)a cada execução e nunca fechava o container resultante — cada chamada vazava um pool inteiro de conexõesdatabase/sqlao Postgres. Uma instância não pareada, que reinicia sozinha no loop de QR, esgotava omax_connectionsdo 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.Containerpor processo, criado viasync.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 viaWHATSAPP_VERSION_*, mas só escrevia o valor emstore.DeviceProps.Version(metadados do device anunciados no pairing) — nunca emstore.waVersionviastore.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 receberevt.Event == "success"o código apenas logava e continuava consumindo o canal em vez de sair. Se um eventocodetardio chegasse depois dosuccess(o canal só fecha quando o whatsmeow decide, não no instante exato doPairSuccess), o contador de QR code continuava incrementando e podia dispararLogout()por "Maximum QR code count reached" sobre uma sessão que tinha acabado de parear com sucesso.Reproduzido em produção:
Correção:
breakao recebersuccess, parando de consumir o canal.Outras mudanças de suporte
go.mod/go.sumatualizados viago mod tidyapós o merge dowhatsmeow-lib, que agora exigego >= 1.26.0.Dockerfile: imagem basegolang:1.25.0-alpine→golang:1.27.1-alpine.pkg/user/service/user_service.go:SetStatusMessagepassou a recebertypes.SetStatusInputem vez destring(API nova do upstream, suporta emoji e duração de status); adaptado o único call-site.Validação
go build ./...ego 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:
Enhancements:
Build:
Tests:
Chores: