2

Pitch: Por que seu push não chegou: os erros do FCM e do APNs, e as falhas que ninguém reporta (com o que a gente aprendeu no Espresso Notify)

Uma notificação push tem duas vidas. Na primeira, seu servidor pede ao Firebase Cloud Messaging (FCM) ou ao APNs da Apple para entregar e recebe uma resposta. Na segunda, um celular em algum lugar decide se mostra ou não. A maior parte das horas perdidas em debug vem de ler a resposta da primeira vida como se ela descrevesse a segunda.

Este texto cobre as duas: o que os erros do FCM (HTTP v1) e do APNs significam, quais você deve repetir, quais são "apague esse token", e as falhas que voltam sem erro nenhum.

"Aceito" não é "entregue"

Quando o APNs responde 200, ele aceitou a sua requisição. Quando o FCM devolve o nome da mensagem, também. Um serviço de web push que responde 201 Created é a mesma coisa: a RFC 8030 diz que a mensagem "foi aceita", só isso.

A documentação da Apple não dá ao servidor que enviou um sinal de que a notificação foi exibida. O FCM é mais generoso, mas também separa as coisas: na exportação para o BigQuery, MESSAGE_ACCEPTED quer dizer que o servidor do FCM recebeu uma requisição válida, e MESSAGE_DELIVERED quer dizer que ela chegou ao SDK do FCM no aparelho, e esse campo não vem exportado por padrão. Os relatórios de entrega ainda dependem do Google Analytics e podem atrasar até 24 horas.

Para o seu pipeline: guarde "aceito pelo provedor" e "entregue/aberto" como fatos diferentes. O primeiro vem da resposta do provedor. Os outros dois só existem se o app avisar o servidor.

Coloque todo erro em um de quatro baldes

Antes das tabelas, uma regra que economiza horas. Todo erro de provedor cai em um destes:

  1. O token morreu. Apague e nunca mais envie para ele.
  2. A sua requisição está errada. Corrija; repetir a mesma requisição repete a falha.
  3. Credencial ou configuração errada. Conserte o setup; isso afeta muitos aparelhos de uma vez.
  4. O provedor está com problema, ou você está indo rápido demais. Tente de novo depois, com backoff exponencial e jitter.

Misturar os baldes é como times acabam apagando tokens saudáveis durante uma instabilidade, ou repetindo para sempre o envio a um token morto.

FCM (HTTP v1)

ErroHTTPO que significaO que fazer
UNREGISTERED404O token não vale mais: o app foi desinstalado, cancelou o registro ou o token expirouApague o token
INVALID_ARGUMENT400Algum parâmetro é inválido: token malformado, nome de pacote errado, mensagem grande demais, chave de dados reservada, TTL inválidoLeia o detalhe do BadRequest. Corrija a mensagem. Só apague o token depois de confirmar que o payload está válido
SENDER_ID_MISMATCH403O remetente com que você se autenticou não é o remetente para o qual o token foi registradoConfira se a service account é do mesmo projeto Firebase do app. Não apague tokens antes disso
QUOTA_EXCEEDED429Taxa excedida para o projeto, para um aparelho ou para um tópicoReduza a taxa. O FCM recomenda backoff exponencial com atraso inicial de pelo menos 1 minuto para taxa de mensagens e de tópico
UNAVAILABLE503O FCM está sobrecarregadoRepita a mesma requisição, respeite o Retry-After se vier, use backoff exponencial e jitter
INTERNAL500Erro interno desconhecido do FCMRepita com backoff; veja a página de status do FCM; se persistir, fale com o suporte do Firebase
THIRD_PARTY_AUTH_ERROR401O certificado do APNs ou a chave de web push está inválida ou ausente, para uma mensagem a iOS ou a um registro webCorrija as credenciais de APNs ou de web push no console do Firebase

Três detalhes da documentação oficial que passam batido:

  • INVALID_ARGUMENT é o ambíguo. O guia de tokens do próprio FCM diz que pode ser registro inválido ou payload ruim, e orienta tratar como token morto só quando você confirmou que o payload está certo. Registre o detalhe do campo antes de apagar qualquer coisa.
  • O limite de tamanho é 4096 bytes para a maioria das mensagens e 2048 bytes para mensagens a tópicos, contando chaves e valores. O TTL é um inteiro entre 0 e 2.419.200 segundos (4 semanas).
  • No Android, o FCM expira um registro após 270 dias de inatividade. No iOS ele depende do APNs, que não tem essa expiração, então o Firebase recomenda que você mesmo limpe tokens antigos.

APNs

StatusreasonO que significaO que fazer
400BadDeviceTokenO token é inválido, ou não bate com o ambientePrimeiro confira sandbox vs produção. Só depois trate como morto
400DeviceTokenNotForTopicO token não pertence ao topic (bundle ID) que você enviouCorrija o apns-topic. A Apple lista entre os erros que não se repete como está
400TopicDisallowedEnviar para esse topic não é permitidoConfira se o topic é permitido para o seu app e a sua chave
403ExpiredProviderTokenO seu JWT está vencidoGere outro e repita
403InvalidProviderTokenO JWT é inválido ou a assinatura não pode ser verificadaConfira a chave, o Key ID e o Team ID
410UnregisteredO token não está mais ativo para o topic; a resposta traz um timestampPare de enviar para ele
413PayloadTooLargeO payload passou do limiteReduza. 4 KB (4096 bytes), 5 KB para VoIP
429TooManyRequestsRequisições demais para o mesmo token de aparelhoRepita com atraso
429TooManyProviderTokenUpdatesVocê renova o JWT com frequência demaisRenove no máximo uma vez a cada 20 minutos
500 / 503InternalServerError, ServiceUnavailable, ShutdownProblema do lado da AppleRepita com backoff

As regras nas palavras da própria Apple valem ficar perto do código: não repita BadDeviceToken, DeviceTokenNotForTopic, Forbidden, ExpiredToken, Unregistered nem PayloadTooLarge; TooManyRequests pode ser repetido com atraso; respostas 5xx podem ser repetidas, e a Apple fala em esperar 15 minutos e usar back-off.

Mais duas coisas dessa página. Erros 4xx te atrasam: o APNs pode derrubar a conexão de um provedor que produz erros demais, e derruba mais cedo no caso de BadDeviceToken. Enviar para token morto não é só desperdício, é frear o seu tráfego bom. E o BadDeviceToken tem uma causa clássica: token de build do Xcode (desenvolvimento) pertence ao ambiente sandbox, enquanto builds do TestFlight e da App Store pertencem à produção. Enviou para o host errado, todo token do outro tipo falha.

Para o seu pipeline: mapeie cada código para um balde em um lugar só (uma tabela de consulta, não ifs espalhados) e registre o código bruto ao lado da sua classificação. Quando aparecer um motivo novo, o padrão deve ser "não apaga, não repete às cegas, alerta".

Falhas que voltam sem erro

Esta é a parte em que as tabelas não ajudam. O provedor disse sim, e o usuário não viu nada.

As notificações estão desligadas para o app. No Android 13 e posteriores, notificações vêm desligadas por padrão numa instalação nova, e se o usuário toca em "Não permitir", todos os canais ficam bloqueados. O token continua válido e o envio continua sendo aceito. O FCM ainda avisa que mensagens de alta prioridade que não geram notificação visível podem ser rebaixadas numa janela de 7 dias. Chame areNotificationsEnabled() no app e mande o resultado ao servidor junto com o token. Não perturbe e modos de Foco funcionam igual do ponto de vista do servidor: o sistema decide o que mostrar, e nada avisa você.

Doze e prioridade. Mensagens de prioridade normal do FCM podem ficar retidas enquanto o aparelho está em Doze; a prioridade alta é a opção pensada para conteúdo urgente e visível. O APNs tem a mesma ideia: apns-priority 10 é "agora", 5 é "quando a energia permitir", e 5 ou 1 podem ser agrupadas em rajadas ou limitadas. Escolha a prioridade por tipo de mensagem, não um padrão global.

Expiração com o celular offline. Sem TTL, o FCM pode guardar uma mensagem por até quatro semanas; com TTL 0, descarta o que não consegue entregar na hora. O APNs com apns-expiration: 0 tenta uma vez e não armazena. Se o push de "a promoção acaba em 1 hora" ficou na fila um dia, ele deveria ter expirado; defina o TTL de propósito.

Fabricantes de Android que matam apps. Alguns fabricantes encerram apps em segundo plano de forma agressiva para poupar bateria. O site da comunidade dontkillmyapp.com coloca Xiaomi, Samsung, OnePlus e Huawei na pior nota. Você não resolve isso pelo servidor, mas dá para detectar o padrão: se as suas estatísticas de entrega estão boas no geral e ruins para uma marca de aparelho, esse é o primeiro suspeito. Aponte os usuários para os passos específicos de cada fabricante na sua página de suporte.

Expo Go. Se você testa com Expo Go, o push remoto não está disponível lá no Android a partir do SDK 53; o Expo diz que é preciso um development build, e as notificações locais continuam funcionando no Expo Go. "Funciona no meu build e não no Expo Go" é comportamento esperado.

Web push. Três coisas pegam todo mundo. No iOS o pedido de permissão precisa vir de uma interação direta do usuário, e pedir a partir de um botão é o padrão seguro em qualquer lugar. O push precisa de um service worker registrado. E no iOS e iPadOS 16.4 ou posterior, o web push funciona para web apps adicionados à Tela de Início, não para uma aba comum do Safari. Quando uma assinatura expirou, a RFC 8030 diz que o serviço de push responde 404: apague a assinatura.

Uma ordem de diagnóstico que funciona

  1. O provedor aceitou a requisição? Leia o status e o reason/código de erro, não só "sucesso".
  2. O token está vivo? Veja a idade dele e quando o app o registrou pela última vez.
  3. Ambiente certo? Sandbox vs produção no APNs, projeto Firebase certo no FCM.
  4. O payload está abaixo do limite, com TTL e prioridade razoáveis?
  5. O app pode mostrar notificações nesse aparelho (permissão, canal, modos de foco)?
  6. O aparelho é de uma marca que mata apps, ou o build não recebe push remoto (Expo Go)?
  7. Se você controla o app, ele reportou o recebimento? Se não, você está chutando.

Sobre o projeto

Eu trabalho no Espresso Notify, um serviço de push para iOS, Android e web com dashboard e API REST, e os casos acima moldaram a forma como ele reporta uma falha. Cada aparelho que falha recebe um código estável e o que fazer a respeito, em vez de uma string crua do provedor; tokens que o provedor diz estarem mortos são removidos para o próximo envio pular; e a documentação diz com todas as letras que "enviado" significa que o provedor aceitou. É grátis até 10.000 aparelhos.

Uma pergunta para os comentários: quando o FCM devolve INVALID_ARGUMENT, como vocês decidem no pipeline entre "payload ruim" e "token morto"? Lê o detalhe do BadRequest ou espera uma segunda falha?

Carregando publicação patrocinada...
1

Sobre a pergunta do fim: se não me engano, quando o problema é o token o BadRequest vem com o fieldViolations apontando pro campo message.token. Se for outro campo, é payload, e eu nem contaria como falha do token. Esperar a segunda falha ajuda pouco, porque payload ruim falha igual toda vez.

No Unregistered do APNs eu usaria o timestamp antes de apagar. Se o app registrou o token de novo depois daquele horário, ele voltou a valer, e apagar seria perder um aparelho bom. Era a mesma regra que a Apple dava no antigo feedback service.

1

Boa, as duas observações são certeiras.

No FCM é isso mesmo: o detalhe google.rpc.BadRequest traz fieldViolations[].field, e quando ele aponta para message.token o problema é o token; qualquer outro campo é payload. Aqui a gente separa olhando para qual campo o erro aponta: se é um campo da mensagem (message.android, message.data, message.notification…), o token fica intacto e o erro vai para o log do envio com o texto do FCM. E concordo que esperar a segunda falha não ajuda no caso de payload, porque ele falha igual toda vez.

No Unregistered, o timestamp é o que separa "token morto" de "token que voltou". A regra do antigo feedback service continua valendo: só apaga se o app não registrou o token de novo depois daquele horário. Comparar com o horário do último registro resolve a corrida entre o app se registrar e a resposta do APNs chegar.

Valeu por complementar, deixa o texto mais completo pra quem chegar depois.

1
2

Valeu, Oletros! Ótima pergunta, e a resposta curta é: depende do problema que você quer resolver.

Novu é infraestrutura de notificações multicanal e open source: você descreve fluxos (e-mail, SMS, chat, inbox, push) e ele orquestra usando os provedores que você conectar. Se a prioridade é hospedar tudo e controlar cada peça, é uma ótima escolha, e aí a conta inclui subir, atualizar e monitorar essa infra.

OneSignal é a referência de mercado em engajamento. Pela página de preços deles hoje, o plano grátis cobre push mobile para até 1.000 usuários ativos por mês.

O Espresso Notify nasceu para fazer push muito bem feito, gerenciado, sem você cuidar de infra:

  • Preço que acompanha o app, não o volume de mensagens: grátis até 10 mil aparelhos ativos, com envio ilimitado; 25 mil aparelhos por US$ 9,99/mês e 100 mil por US$ 29,99/mês. Mandar mais campanhas não muda a conta.
  • Engajamento já no produto: campanhas e transacionais, jornadas com espera por evento e ramificação, A/B com vencedor automático, entrega no fuso de cada pessoa e no melhor horário, horário de silêncio, limite de frequência, grupo de controle e receita atribuída.
  • Entrega que dá pra ler: cada aparelho volta com um código estável e o que fazer a respeito (token morto, credencial, payload), em vez da string crua do FCM ou do APNs.
  • Feito para quem integra: React Native configurado com um comando (Expo ou React Native CLI), SDK web, API REST com chave de idempotência em todo envio (retry nunca notifica duas vezes) e webhooks assinados.

E no que a gente está trabalhando agora:

  • Mais SDKs no app: nativos em Swift e Kotlin, Flutter e outras tecnologias, para você integrar o push na stack que já usa.
  • Mais bibliotecas no servidor: hoje o backend já integra pela biblioteca Node, e estamos preparando Go e outras linguagens.
  • Mais canais no mesmo lugar: e-mail, SMS e mensagens dentro do app, ligados ao mesmo usuário, para você falar com uma pessoa por várias frentes a partir de um painel e uma API só.

Se você tem um app React Native ou web e quer testar, o plano grátis já cobre um app em produção de bom tamanho. E qualquer feedback, inclusive sobre o que sentir falta, é muito bem-vindo por aqui.

1

Meus 2 cents,

Obrigado pela pronta resposta !

Tenho alguns sistemas de monitoramento que enviam avisos (p.ex. queda de link em roteador mikrotik) via whatsapp na API nao oficial, mas tenho tido diversos problemas (principalmente numeros sendo banidos, mesmo enviando poucas msgs).

Uso tambem o telegram e slack para entrega de msgs, mas tenho pensado em testar alternativas e me pareceu interessante.

Estou anotando para testar (via REST API provavelmente).

Saude e Sucesso !


Este post foi favoritado via extensão TABNEWS FAVORITOS

Tem curiosidade sobre IA ? Da uma olhada no meu LIVRO: IA PARA ENGENHEIROS