iachat/docs/troubleshooting/media-audio-imagem-whatsapp.md

7.0 KiB

Troubleshooting: Áudio e Imagem não aparecem no Chatwoot (WhatsApp via WuzAPI)

Data: 2026-02-28 Branch: fix-media-audio-v2 Canal: WhatsApp via WuzAPI


Sintomas

  • Áudio exibe 00:00 / 00:00 e não toca
  • Imagem exibe "Esta imagem não está mais disponível"
  • Mensagens de texto chegam normalmente

Diagnóstico — Root Causes

Havia três bugs independentes, todos precisavam ser corrigidos.


Bug 1 — URL de mídia apontando para localhost (invisível via ngrok)

Arquivo: app/models/attachment.rb

Problema: file_url e thumb_url geravam URLs hardcoded com localhost:3000. O browser acessando via ngrok não consegue resolver localhost:3000 diretamente, então <img> e <audio> falhavam silenciosamente.

# ANTES (quebrado):
rails_storage_proxy_url(file, host: 'localhost:3000', protocol: 'http')

# DEPOIS (correto):
rails_storage_proxy_url(file, **dev_url_options)

def dev_url_options
  uri = URI.parse(ENV.fetch('FRONTEND_URL', 'http://localhost:3000').chomp('/'))
  host = [80, 443].include?(uri.port) ? uri.host : "#{uri.host}:#{uri.port}"
  { host: host, protocol: uri.scheme }
end

Efeito visual:

  • Imagem → @errorhasError = true → "Esta imagem não está mais disponível"
  • Áudio → onLoadedMetadata nunca disparado → duration = 0 → "00:00 / 00:00"

Bug 2 — Arquivo de mídia salvo criptografado (sem descriptografar)

Arquivo: app/services/whatsapp/incoming_message_wuzapi_service.rb + decryption_service.rb

Problema: O fluxo de download tinha 3 métodos em cascata:

Método O que faz Status
Method 1 WuzAPI /chat/downloadimage endpoint Sempre falhava com 502 Bad Gateway
Method 2 DecryptionService com Net::HTTP Net::HTTP não segue redirects — CDN do WhatsApp redireciona → nil silencioso
Method 3 Down.download direto do CDN ⚠️ Baixava mas salvava bytes encriptados

O WhatsApp entrega mídia encriptada (AES-256-CBC) no CDN com URLs .enc. O Method 3 baixava os bytes encriptados corretamente mas os salvava sem descriptografar. O arquivo no disco era inválido.

Diagnóstico confirmado via xxd:

# JPEG começa com FF D8 FF
# Arquivo salvo começava com:
00000000: 3188 d20c 46ae 98f3 03bd...  ← bytes encriptados

Correção: Unificar Methods 2+3: usar Down.download (que segue redirects) para baixar os bytes, depois descriptografar em memória antes de salvar.

# DEPOIS:
encrypted_tempfile = Down.download(media_url, ...)
encrypted_bytes = encrypted_tempfile.read.b

if attachment_data[:media_key].present?
  decrypted = Whatsapp::DecryptionService.new(
    attachment_data[:media_key],
    file_content_type(message_type)
  ).decrypt_bytes(encrypted_bytes)
  return decrypted if decrypted
end

StringIO.new(encrypted_bytes)  # fallback

Bug 3 — OpenSSL::Cipher#update! não existe em Ruby

Arquivo: app/services/whatsapp/decryption_service.rb

Problema: O método de descriptografia usava decipher.update!(data) mas Ruby só tem decipher.update(data) (sem !). Causava NoMethodError em todo attempt de descriptografia.

# ANTES (quebrado):
decipher.update!(data) + decipher.final

# DEPOIS (correto):
decipher.update(data) + decipher.final

Estado correto após os fixes

app/models/attachment.rb

  • file_url e thumb_url usam dev_url_options que lê FRONTEND_URL do .env
  • Em produção (else): comportamento inalterado com url_for(file)

app/services/whatsapp/incoming_message_wuzapi_service.rb

  • Method 1: WuzAPI endpoint (funciona quando disponível)
  • Method 2+3 unificados: Down.download → decrypt com mediaKey → fallback raw

app/services/whatsapp/decryption_service.rb

  • Constructor aceita (media_key, media_type) — sem URL (download separado)
  • Método principal: decrypt_bytes(encrypted_bytes) — recebe bytes já baixados
  • Algoritmo: HKDF SHA-256 → AES-256-CBC → fallback AES-256-CTR
  • Loga os primeiros bytes se o formato não for reconhecido (facilita debug futuro)

Checklist de diagnóstico (quando áudio/imagem não funcionar)

1. Verificar URLs geradas

grep "data_url" log/development.log | tail -3

Deve aparecer: https://SEU-NGROK.ngrok-free.dev/rails/active_storage/... Se aparecer http://localhost:3000/... → Bug 1 voltou (checar FRONTEND_URL no .env)


2. Verificar se o arquivo está encriptado

# Pegar o blob key do arquivo (via Rails console ou log)
xxd storage/XX/XX/BLOB_KEY | head -2
  • FF D8 FF → JPEG válido
  • 89 50 4E 47 → PNG válido
  • 4F 67 67 53 (OggS) → OGG válido
  • Qualquer outra coisa → encriptado

3. Verificar logs de descriptografia

grep -E "WuzAPI Decrypt|SUCCESS|invalid format|first bytes" log/development.log | tail -10
  • WuzAPI Decrypt: SUCCESS - Valid media detected
  • WuzAPI Decrypt: Decrypted but invalid format (first bytes: XX XX XX XX) → algoritmo errado — os bytes revelam o tipo real
  • WuzAPI Decrypt Error: NoMethodError → bug no Ruby (checar update vs update!)
  • Nada aparece após "Attempting local decryption" → mediaKey ausente no payload do WuzAPI

4. Verificar se o Sidekiq está rodando e processando

ps aux | grep sidekiq | grep -v grep

Deve aparecer somente uma linha com [N of 12 busy] sem stopping.

Se aparecer stopping ou dois processos:

pkill -f sidekiq
rm -f .overmind.sock
pnpm run dev

5. Verificar se o arquivo existe no disco

# No Rails console:
Attachment.last.file.attached?  # deve ser true
Attachment.last.file.blob.service.exist?(Attachment.last.file.blob.key)  # deve ser true

ActiveStorage::FileNotFoundError no log = blob existe no banco mas arquivo não está no disco = o upload falhou silenciosamente.


6. Verificar status do WuzAPI endpoint de download

grep "WuzAPI: Endpoint download failed" log/development.log | tail -3

Se aparece 502 Bad Gateway sistematicamente → WuzAPI /chat/downloadimage está down. Isso é esperado e o sistema cai automaticamente para Method 2+3 (download direto + decrypt).


Dependências críticas

Variável Onde Valor esperado em dev com ngrok
FRONTEND_URL .env https://SEU-URL.ngrok-free.dev
ACTIVE_STORAGE_SERVICE .env local
mediaKey Payload WuzAPI Obrigatório para descriptografia

Notas de arquitetura

  • O WhatsApp sempre encripta mídia no CDN (arquivos .enc)
  • A chave (mediaKey) é entregue no payload do webhook junto com a URL
  • Sem mediaKey não é possível descriptografar — o arquivo vai aparecer corrompido
  • O WuzAPI endpoint /chat/downloadimage deveria retornar mídia já descriptografada (Method 1), mas está com instabilidade (502). O fallback via HKDF+AES é a solução robusta.
  • Em produção (sem ngrok), o Bug 1 não existe pois url_for(file) usa default_url_options configurado corretamente.