Lib de App Router adotada no Pages Router falhou em silêncio

TLDR: adotamos next-runtime-env@3.x para resolver env em runtime. A 3.x é a linha de App Router; o app é Pages Router. Nada foi emitido no HTML, Env devolveu undefined sem erro, e testes, lint e build passaram verdes — o fix foi mergeado sem funcionar.

O que aconteceu

O login e o painel no k8s davam 404 porque a API URL era inlinada em build time e a imagem é buildada sem ela. A correção adotou next-runtime-env e foi mergeada com suíte verde, lint limpo e build passando. Não funcionava.

Por que passou despercebido

Três camadas de silêncio empilhadas:

  1. A lib estava fora da matriz de suporte. O README dela é explícito: 1.x é Pages Router (Next 12/13), 3.x é App Router (Next 14). O spec original afirmou que “suporta Next 15 + pages-router” — afirmação nunca verificada, e falsa.
  2. Duas cópias do Next. A lib declara next: ^14 em dependencies (não só em peerDependencies), então o npm aninhou um next@14.2.35 dentro dela — 215 MB. O next/script que ela importa era de outra instância do Next, e beforeInteractive depende do componente se registrar no runtime que renderiza o _document. O registro nunca chegou.
  3. A detecção de browser dependia do valor que ela não conseguia injetar.

    js function isBrowser() { return Boolean(typeof window !== 'undefined' && window['__ENV']); }

    Sem __ENV, isBrowser() devolve false dentro do browser, e env() cai no ramo de servidor lendo process.env — que no bundle client vira undefined. Sem exceção, sem log.

Por que a suíte não pegou

Os testes exercitavam a classe Env com a lib mockada. O build só provava que a URL não estava mais inlinada — ausência de evidência tratada como evidência de sucesso. Ninguém subiu o servidor e olhou se o browser recebia o valor. O bug só apareceu quando o usuário pediu para rodar e ver funcionando.

O que fazer diferente

  • Verificar a matriz de compatibilidade da dependência antes de adotá-la, não depois. Uma consulta ao README e ao peerDependencies teria matado a escolha em dois minutos.
  • Desconfiar de dependencies onde deveria haver peerDependencies em libs de framework: é o sinal de que uma segunda cópia do framework vai entrar no node_modules.
  • Um mecanismo que atravessa server→browser precisa de um teste que atravesse server→browser. Teste unitário com a lib mockada não prova integração. Hoje existe src/infra/env/runtimeEnvScript.test.ts, que assere sobre o HTML realmente emitido.
  • Preferir falha ruidosa a fallback silencioso. Todo o custo veio de undefined ser um valor aceitável onde deveria ser um erro.

Achado colateral: o contrato de URL ambíguo

Na mesma investigação apareceu que o código mistura ${API_URL}path e ${API_URL}/path — o mesmo arquivo usa as duas formas. Sobreviveu porque .env.example tinha barra final e o secret do k8s não. Consequência: phone auth e cadastro inicial já estavam quebrados em staging, por um motivo diferente do 404. Contrato de formato de URL precisa ser único e aplicado por uma função, nunca por convenção.

Referências