Frontend runtime API URL via next-runtime-env (corrige 404 client-side no k8s)
TLDR: As chamadas de API no client 404am no k8s porque
API_URLé inlinada em build time e sai vazia (a imagem do k8s é buildada sem ela). Corrigido comnext-runtime-env: o pod serveNEXT_PUBLIC_API_URLlida em RUNTIME (a partir do secret), então o browser chama o domínio real da API diretamente — sem build-arg, sem imagem por ambiente, sem proxy. Também torna permanente oproxy-buffer-sizedo ingress.
Module: frontend
Contexto
Depois que o frontend do trgclub foi migrado para o k8s (trgclub--staging,
beta.staging.trgclub.com), o login funciona mas o app falha em duas coisas:
-
502 na navegação pós-login — o cookie de sessão do next-auth tem ~3.7KB e o ingress nginx do frontend-web não tem
proxy-buffer-size, então o nginx retorna 502 antes da requisição chegar ao pod. Validado ao vivo:proxy-buffer-size: 16kno ingress de staging corrige (aplicado via kubectl, ainda não está no git → o próximo terraform/delivery apaga a mudança). -
404 nas chamadas de API client-side — as chamadas do browser no dashboard (
/features/:id,/me/profile,closest_meetings,latest_meetings,performance) batem emhttps://beta.staging.trgclub.com/features/...(origem do frontend, sem/api/v1) e 404am. São hooks react-query rodando no browser (ControlPanel.tsx,HistoryPerformance.tsx,services/user,services/meetings) →fetcher→fetchInstance. Causa raiz:src/services/fetch/fetch.tsdefinebaseURL = process.env.API_URL, enext.config.jsexpõeAPI_URL/NEXT_PUBLIC_API_URLvia o blocoenv— ambos inlinam o valor em build time. A imagem do k8s é buildada SEMAPI_URL(só é injetada em runtime viatrgclub-frontend-secrets), então o bundle do client sobe comundefined→ o axios cai de volta para a origem atual. Verificado: nenhum chunk servido contém a API URL.Chamadas server-side (getServerSideProps em
painel-de-controle,authorizedo next-auth, route guards) já funcionam — leemprocess.env.API_URLem runtime no servidor. Só o código do browser está quebrado:process.envnão existe no browser, então a API URL precisa viajar do servidor (onde o env existe) para o browser em runtime.
Por que não build-arg / NEXT_PUBLIC_ inlinado: o Netlify builda por site com
API_URL presente, então inlinar funciona lá. O pipeline k8s da WeHive
(package.yml) builda a imagem uma vez, sem input de environment, e promove
a mesma imagem para staging e production. Inlinar a URL de um ambiente em build
time faria production chamar a API de staging. Por isso a URL precisa ser
resolvida em runtime, mantendo a imagem agnóstica de ambiente.
O CORS já está aberto no backend (access-control-allow-origin: *, headers de
auth expostos), então o browser chamando o domínio da API diretamente
(beta.staging.api.trgclub.com/api/v1) funciona — sem necessidade de proxy. O
requisito é: o browser chama o backend diretamente, com a URL fornecida em
runtime.
Objetivos
- Browser chama o domínio da API do backend diretamente (
.../api/v1), com a URL resolvida em runtime — fornecida pelo pod em execução a partir do seu secret, sem inlining em build. Mesma imagem funciona em todo ambiente sem rebuild. - Usar
next-runtime-env(padrão da comunidade para runtime env no Next; suporta Next 15 + pages-router) em vez de umwindow.__ENV__/env.jsartesanal. A lib serve valoresNEXT_PUBLIC_*lidos do processo do pod no momento da requisição. - Reaproveitar o secret
API_URLexistente — o pod derivaNEXT_PUBLIC_API_URLa partir dele no boot (run/runtime), sem adicionar nova chave de secret/terraform (única fonte da verdade). - Encapsular em um módulo
Envpequeno (Env.get("API_URL")) para que os consumidores nunca toquem na lib ou na convençãoNEXT_PUBLIC_diretamente. - Unificar em uma única variável de runtime
NEXT_PUBLIC_API_URL, lida viaenv()em todo lugar (server e client resolvem em runtime). Aposentar oAPI_URLseparado, server-only. - Tornar
proxy-buffer-sizepermanente no manifest do ingress do frontend (sobrevive a terraform/delivery), igualando o valor validado em staging.
Fora de escopo
- Aurora vazia do backend / login 500 — problema separado; o backend de beta temporariamente aponta para o Heroku; migrar os dados para uma Aurora na própria conta AWS do trgclub é um esforço planejado distinto. Não é tratado aqui.
- CrashLoopBackOff em production
beta.trgclub.com— quebra pré-existente separada em production; fora de escopo. - Sem mudança de pipeline (sem input
environmentempackage.yml). - Sem proxy/rewrite (browser bate na API diretamente, conforme requisito); sem mudança de backend/auth; sem redução do cookie de sessão (o ajuste do buffer cobre o 502).
Mudanças
modules/frontend/package.json
- Adicionar dependência
next-runtime-env.
modules/frontend/pages/_document.tsx
- Importar
PublicEnvScriptdenext-runtime-env; renderizar<PublicEnvScript />no<Head>(antes das outras tags de head), para que o pod injeteNEXT_PUBLIC_*no momento da requisição (runtime), legível pelo bundle do client.
Nova abstração Env — src/infra/env/
Encapsular next-runtime-env num módulo pequeno para que o resto do código nunca
conheça a lib ou a convenção NEXT_PUBLIC_. Consumidores chamam Env.get("API_URL").
```ts // src/infra/env/index.ts import { env } from “next-runtime-env”;
const PUBLIC_PREFIX = “NEXT_PUBLIC_”;
export const Env = {
get(key: string): string | undefined {
// “API_URL” -> NEXT_PUBLIC_API_URL, resolved at RUNTIME (server + client);
// fall back to the bare key for server-only vars.
return env(${PUBLIC_PREFIX}${key}) ?? env(key);
},
getOrThrow(key: string): string {
const value = Env.get(key);
if (!value) throw new Error(Missing env: ${key});
return value;
},
};
```
Por que funciona com chave dinâmica: o env() do next-runtime-env lê de um
objeto de runtime (client: window.__ENV injetado pelo PublicEnvScript; server:
process.env do pod), NÃO via inlining literal em build time do Next — então
env(\NEXT_PUBLIC_${key}`) resolve corretamente. Benefícios: esconde a lib
(troca num único lugar), esconde o prefixo NEXT_PUBLIC_`, ponto único de
validação/log, mockável em testes.
Consumidores usam Env.get(...) (uma única fonte de runtime, sem split por typeof window)
Os três usos colapsam em Env.get("API_URL"), resolvido em runtime em ambos os
contextos:
src/services/fetch/fetch.ts:const baseURL = Env.get("API_URL")(usado pelofetcherno client e no server). O axios aponta para o domínio real da API.../api/v1; garantir join de path limpo (sem/api/v1duplicado, trailing slash tratado).src/infra/NextAPIAuth/index.js: substituirprocess.env.API_URLporEnv.get("API_URL").src/infra/routesGuard/validators/restrictedRoutesValidator/restrictedRoutesForRolesValidator.tsx: substituirprocess.env.API_URLporEnv.get("API_URL").
Aposentar as leituras diretas de process.env.API_URL (um único acessor canônico:
Env.get). A variável de runtime é NEXT_PUBLIC_API_URL; a camada Env mapeia a
chave amigável "API_URL" para ela.
modules/frontend/next.config.js
- Remover
API_URLeNEXT_PUBLIC_API_URLdo blocoenv— a lib as lê em runtime; deixá-las emenvinlinaria valores de build time e anularia a correção.
modules/frontend/run/runtime — derivar NEXT_PUBLIC_API_URL a partir de API_URL no boot
Em vez de adicionar uma chave nova ao secret, reaproveitar o API_URL existente
do secret do k8s e derivar a variável pública quando o pod inicia (única fonte da
verdade):
bash
#!/usr/bin/env bash
set -e
export NEXT_PUBLIC_API_URL="${API_URL}" # k8s secret already provides API_URL
exec npm run start
No boot isso coloca NEXT_PUBLIC_API_URL no process.env do pod;
o PublicEnvScript do next-runtime-env então serve ao client em runtime (a lib
só expõe NEXT_PUBLIC_*, então nenhum outro secret vaza). Nenhuma mudança de
secret/terraform necessária — API_URL continua a única fonte; a imagem
permanece agnóstica de ambiente (o pod de prod deriva a URL de prod a partir do
secret de prod).
modules/frontend/.infra/k8s/ — ingress
- Adicionar ao ingress do frontend (base ou patch de overlay, seguindo onde o host
é definido):
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"enginx.ingress.kubernetes.io/proxy-buffers-number: "4". Torna a correção do 502 permanente.
Como verificar
- Build:
next buildpassa; grep nos chunks de client buildados — a API URL NÃO deve aparecer (não mais inlinada; é servida em runtime peloPublicEnvScript). - Staging (após deploy): logar em
beta.staging.trgclub.com; o dashboard carrega sem 404s —/features/:id,/me/profile,closest_meetings,latest_meetings,performanceretornam 200 (aba Network), cada requisição indo diretamente parahttps://beta.staging.api.trgclub.com/api/v1/.... - 502 resolvido:
/api/auth/sessione/app/painel-de-controleretornam 200 com o cookie de sessão completo (a anotação de buffer do ingress já está no manifest). - Agnóstico de ambiente: a mesma imagem, dado um
NEXT_PUBLIC_API_URLdiferente no secret de outro ambiente, faz o browser chamar a API daquele ambiente — sem rebuild. Confirmar inspecionando que o script de env servido em runtime difere por ambiente.
Documentação
.project/docs/reference/frontend/frontend_runtime_api_url.mddocumenta a topologia: API base URL resolvida em runtime vianext-runtime-env(NEXT_PUBLIC_API_URLservida pelo pod a partir do seu secret); nunca inlinar API URLs no bundle do client no k8s (modelo build-once/promote). Browser chama o domínio da API diretamente (CORS aberto).- Segue
wehive:infra(anotações de ingress no.infra/k8sdo próprio projeto).