
O 302 que fingiu ser 200
O erro que não estava no formulário
Em produção, todo envio do formulário de contato devolvia a mensagem de erro padrão. O console do navegador não ajudava: o fetch morria com um tipo genérico, sem corpo e sem status útil. O reflexo é abrir o código do formulário — e não havia nada errado ali. O backend respondia. O banco respondia. A tabela de mensagens existia.
Levei mais tempo do que gostaria para aceitar a parte incômoda: o problema não estava em nenhuma das duas pontas que eu conseguia ler no editor. Estava no caminho do meio — o proxy do próprio site e a camada de acesso na frente do domínio.
Separei o caminho em três camadas e testei uma por vez: o formulário no navegador, o proxy same-origin do site, e a API do painel que guarda as mensagens.
Duas armadilhas empilhadas
A primeira já tinha aparecido dias antes e estava documentada num comentário de código. Chamar a API do painel direto do navegador, de outra origem, devolvia erro com corpo vazio e nenhum cabeçalho de origem permitida. O fetch traduzia aquilo como “falha de rede”, que é a mensagem mais mentirosa da lista — falha de rede sugere cabo, não política de origem.
A cura foi um proxy same-origin: uma rota no próprio Next que recebe o pedido do navegador e repassa do lado do servidor, com uma lista fechada de prefixos permitidos e limite de segmentos. Sem lista fechada, um proxy desses vira porta aberta para o outro serviço.
Com o proxy no ar, fazendo o mesmo POST pelo site, o erro continuou. E aí a origem mudou de lugar: uma configuração de acesso criada de madrugada, pensada para proteger o painel administrativo, cobria o domínio inteiro. Toda requisição — inclusive a do prefixo público que o proxy precisa alcançar — recebia redirecionamento para a tela de login. O navegador seguia, era bloqueado por política de origem, e o fetch morria. O backend nunca viu o pedido.
Proteger o domínio inteiro é o default confortável de qualquer serviço de acesso, e é exatamente o que quebra todo endpoint que o site precisa chamar sozinho.
Por que o diagnóstico demorou
Um detalhe de ferramenta custou mais tempo que o bug: a biblioteca HTTP do Python segue redirecionamento por padrão. A sonda que eu usava dizia “200, saudável” — porque estava lendo a página de login, não a API. Status 200 numa tela de login é um 200 legítimo e uma resposta completamente errada.
O jeito certo de sondar é olhar o código real, sem seguir o redirect:
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" \
-X POST "https://<painel>/api/<prefixo-publico>" \
-H "Content-Type: application/json" -d '{}'
# 302 https://<conta>.cloudflareaccess.com/cdn-cgi/access/login/... <- acesso na frente
E a validação que prova que o servidor está de pé sem gravar nada no banco, porque a validação de payload roda antes do handler:
curl -s -o /dev/null -w "%{http_code}\n" -X POST "https://<painel>/api/<prefixo-publico>" \
-H "Content-Type: application/json" -d '{}'
# 422 = chegou no handler de validação (aberto, servidor vivo)
# 302 = login na frente (fechado demais)
# 500 = servidor quebrado de verdade
Três códigos, três significados que não se confundem — antes eu só tinha o binário “funcionou / não funcionou”.
A cura foi infraestrutura, não código
Nenhuma linha do site mudou. O conserto foi criar um app de acesso por caminho: cobertura no prefixo público, com política de bypass, e o prefixo administrativo continuando atrás do login. O app anterior, que cobria tudo, virou o exemplo do que não fazer num domínio que serve API e painel ao mesmo tempo.
Depois disso, quatro provas em sequência: POST direto no backend devolveu 201; POST pelo proxy do site devolveu 201; teste de ponta a ponta num navegador real em produção mostrou a mensagem de sucesso com zero erros de página; e uma linha nova apareceu na tabela de mensagens. Não adianta a API responder se o caminho que o usuário percorre continua quebrado — por isso a prova final foi no navegador, não no curl.
Medi de novo agora: o prefixo público devolve 422 (aberto, validação funcionando) e o administrativo devolve 302 para a tela de login (fechado, como deve ser).
O que fica
Três lições, todas sobre medir:
- Acesso é por caminho, não por domínio. Domínio que serve site, API e painel precisa de política por rota, senão o login come o endpoint público e o sintoma aparece longe, no CORS.
200sem conferir a URL final e o corpo não prova nada. Sonda que segue redirecionamento responde outra pergunta que não a feita.- Erro de CORS é sintoma, quase nunca causa. O
fetchsó sabe dizer “não deu”; o motivo estava três camadas acima, numa configuração que não passa pelo editor.
A sonda deixou de ser um comando que eu lembrava de rodar e virou rotina periódica: bate no prefixo público esperando a resposta de validação e confirma que o administrativo continua fechado. Silenciosa quando está tudo no lugar — alerta que grita toda hora ninguém lê.
Próximo passo
Rodar a mesma checagem do outro lado: varrer os outros subdomínios que ganharam app de acesso nas últimas semanas e descobrir quais endpoints públicos estão atrás de um login que ninguém vê. O formulário foi o primeiro lugar onde a falha doeu — improvável que seja o único.
Leia também
Mesmo projeto, mesma trilha — as histórias se conectam.
- DescobertasA história das Descobertas — MCP, FTS5, ai-jail e as lições que viraram regras3 de ago. de 2026
- DescobertasA capa que não mudava — cache immutable e o fix que foi renomear o arquivo8 de set. de 2026
- DescobertasA indexação que precisava se encontrar sozinha — JSON-LD, verificação e IndexNow8 de out. de 2026
Próximo passo
Se isto te serviu, tem mais: escrevo sobre o mesmo assunto nos outros projetos, e a loja tem as ferramentas que eu uso aqui.
Anotado — aviso quando sair artigo novo.
Sem spam, descadastro de um clique. Também estou aqui: Loja de skills · Portfólio