Portifólio — o proxy que fechou a porta e ainda salvou o formulário
Portfólio·

Portifólio — o proxy que fechou a porta e ainda salvou o formulário

7 min de leitura← Voltar para timeline

A maioria dos problemas que parecem bug de front-end é, na verdade, bug de rede.

O Portifólio rodava em um domínio de hospedagem e o formulário de contato funcionava. Quando passei a atender também pelo domínio próprio, a primeira impressão foi de que tudo continuava igual — mesma interface, mesmo build, mesmas rotas. Não era verdade. O formulário de contato simplesmente parou de enviar mensagens, o registro de download do currículo sumiu do banco e o rastreamento de visitas zerou. No console do navegador, três linhas de erro idênticas: TypeError: Failed to fetch.

O que estava acontecendo de verdade

O formulário não falhava por estar quebrado. Ele nem chegava a enviar a requisição. O navegador fazia uma verificação automática antes do envio — o preflight — e ela voltava com erro, sem o cabeçalho de autorização. O navegador cancelava ali mesmo, antes de tocar nos dados que a pessoa havia digitado. Do lado do servidor do site, não havia registro nenhum: a requisição nunca saiu.

O motivo era uma lista explícita de origens autorizadas, mantida no serviço para onde o site fala. Quando alguém entrava pelo domínio novo, essa lista não reconhecia a origem. Não por má vontade — o domínio simplesmente nunca foi cadastrado ali. E o navegador não distingue “domínio não autorizado” de “servidor fora do ar”: para quem está do outro lado da tela, a diferença é sempre a mesma três-palavras, Failed to fetch.

Esse é o tipo de falha que não aparece em teste automatizado. Os testes rodam no mesmo domínio do serviço de API, onde a origem é sempre a mesma — o problema só existe quando alguém chega por um caminho diferente. Por isso a correção não podia ser validada no lugar errado: ela precisava ser testada a partir de uma origem que não fosse a do próprio serviço.

A correção

A solução foi não pedir ao navegador que atravessasse a fronteira de domínios. Em vez disso, criei uma rota intermediária no próprio site: o navegador fala apenas com a origem dele, e o servidor fala com o serviço. Quando duas máquinas conversam, política de origem cruzada simplesmente não se aplica — a checagem acontece no navegador, e o navegador deixou de cruzar fronteiras.

O truque está em como a rota decide o que pode passar. Ela não confia em nada do que chega na URL. Cada pedaço do caminho é decodificado repetidamente até não mudar mais, e só depois é verificado: um pedaço que resolve para . ou .., ou que contém barra invertida, é rejeitado de imediato.

~/lifelog — bash
$cat about.txt
╔══════════════════════════════════════╗
║  Samuel Medeiros                    ║
║  Senior Software Engineer           ║
║  Stack: Python · TypeScript · Rust  ║
║  Projetos: Arachne, Dogwalk,        ║
║            Capivara, TatuEngine      ║
╚══════════════════════════════════════╝
      
$

A lista de prefixos é fechada de propósito: dois caminhos liberados, nada mais. Sem ela, a rota seria apenas um intermediário até qualquer endereço — trocar o destino do proxy seria uma linha de código, e o formulário de contato, o rastreador de visitas e o serviço de mensagens ficariam pendurados no mesmo fio. Fechar a lista é o que separa “proxy seguro” de “proxy aberto”.

O detalhe que quase passou

Um detalhe nessa validação quase passou, e é o tipo de detalhe que só aparece quando você já teve o mesmo problema antes.

Poucos dias antes, um guarda de segurança do próprio projeto — o que impede o download direto dos documentos do currículo — foi contornado por codificação de porcentagem na URL. O guarda comparava a string exatamente como recebia, e um valor codificado passava batido na comparação mesmo apontando para o mesmo arquivo. Não era uma falha nova; era a mesma aula, aplicada em outro lugar.

Por isso a rota intermediária não decodifica uma vez só. Decodifica em laço, até a string estabilizar, e só depois valida. Um caminho como %252e%252e — que é .%2e, que por sua vez é .. — fica dois níveis de codificação mais fundo, e só aparece inteiro na volta final. Quem decodifica uma vez estava vulnerável a quem pensasse nisso.

O que mudou depois

O formulário voltou a enviar mensagens, o registro de downloads voltou a contar e o rastreamento voltou a aparecer no painel. E o ganho maior foi de arquitetura: o site deixou de depender de configuração espalhada em outro serviço para funcionar. A lista de origens autorizadas continua existindo lá — mas agora ela protege o serviço contra chamadas diretas de fora, em vez de ser a única coisa segurando o site de pé.

Um detalhe de processo também saiu daí. O Portifólio passou a ter um alarme próprio que confere, a cada rodada de verificação automática, se as chamadas entre os serviços continuam funcionando. Não substitui teste nenhum — mas pega a regressão no mesmo dia, antes que alguém perceba no formulário que o botão parou de responder.

O que eu levaria para o próximo projeto

Duas regras. Primeira: nunca confie em teste automatizado para provar que uma integração entre domínios funciona. Teste a partir de uma origem diferente, ou o teste vai passar enquanto a pessoa está vendo um erro. Segunda: quando a validação de uma entrada depender de comparação de string, decodifique até estabilizar antes de comparar. A comparação é a última linha de defesa — não a primeira.

O erro não era de interface, nem de framework, nem de configuração do site. Era um domínio novo que ninguém atualizou em outro lugar. A correção levou uma tarde; o que demorou foi entender que o problema não era visível de dentro da aplicação.

Widget de terminal

Os comandos abaixo mostram, direto do código, as três decisões que sustentam o proxy: o destino, a lista fechada de caminhos e a validação dos segmentos.

~/lifelog — bash
$cat about.txt
╔══════════════════════════════════════╗
║  Samuel Medeiros                    ║
║  Senior Software Engineer           ║
║  Stack: Python · TypeScript · Rust  ║
║  Projetos: Arachne, Dogwalk,        ║
║            Capivara, TatuEngine      ║
╚══════════════════════════════════════╝
      
$