Codificação de URL: por que espaço vira %20 e o + causa bugs
Em algum lugar de todo código existe um bug em que o e-mail do usuário chega com o mais removido. [email protected] vira maria [email protected], a busca falha, e ninguém consegue reproduzir porque ninguém do time usa endereço com mais.
A causa é que URLs são regidas por duas regras de codificação que concordam em tudo, menos num caractere.
Por que a codificação existe
Uma URL não é texto livre. Certos caracteres são estruturais: ? inicia a query, & separa parâmetros, = divide nome de valor, / separa segmentos de caminho, # começa o fragmento. Outros não conseguem viajar em segurança — um espaço encerraria a URL numa linha de requisição HTTP antiga, e caracteres não-ASCII não têm representação de bytes acordada.
A codificação por porcentagem resolve os dois. Pegue os bytes do caractere e escreva cada um como % seguido de dois dígitos hexadecimais. Espaço é o byte 0x20, então vira %20. é em UTF-8 são dois bytes, 0xC3 0xA9, então vira %C3%A9.
Uma vez codificado, o caractere fica inerte. %26 dentro de um valor não pode ser confundido com o & que separa parâmetros — o que é exatamente o objetivo.
De onde vem o mais
A codificação por porcentagem é definida pela RFC 3986 e vale para URLs em geral. Por ela, espaço é %20, sempre.
Mas formulários HTML não usam a RFC 3986 nas suas query strings. Usam application/x-www-form-urlencoded, um formato anterior a ela, que codifica espaço como +.
Então os dois abaixo significam "olá mundo":
?q=ol%C3%A1%20mundo
?q=ol%C3%A1+mundo
E aqui está a consequência: sob a codificação de formulário, um mais literal precisa ser escrito %2B, porque um + cru já significa espaço. Sob a RFC 3986, um + cru é apenas um mais.
A mesma string decodifica de formas diferentes conforme a regra que o decodificador segue. É esse o bug inteiro.
O caso do e-mail
[email protected] enviado por um formulário é corretamente codificado como:
maria%2Bcobranca%40exemplo.com
Se alguma camada codifica com as regras da RFC 3986, o mais não é especial, então ele viaja literal:
[email protected]
Agora um decodificador de formulário lê esse + cru como espaço e entrega à sua aplicação maria [email protected]. Nada deu erro. Um caractere mudou de significado silenciosamente entre dois componentes que estavam, cada um, se comportando corretamente.
Dá para ver isso acontecer num codificador/decodificador: codifique um mais, decodifique de volta pelas duas interpretações, e a divergência fica óbvia em uns dez segundos.
Qual função de JavaScript usar
A plataforma te dá três, e elas diferem no que consideram seguro deixar em paz.
encodeURIComponent — codifica tudo exceto A-Z a-z 0-9 - _ . ! ~ * ' ( ). Codifica &, =, ?, / e #. É o que você quer para um valor de parâmetro isolado.
encodeURI — deixa os caracteres estruturais intactos porque espera uma URL inteira. Use para tornar segura uma URL já montada, nunca para um valor.
URLSearchParams — monta a query string para você e aplica codificação de formulário, ou seja, escreve espaço como + e mais literal como %2B.
O erro que produz o bug do e-mail:
// Errado: um & dentro do valor encerra o parâmetro.
const url = `/busca?q=${query}`;
// Certo para um valor:
const url = `/busca?q=${encodeURIComponent(query)}`;
// Certo para vários, e trata o + corretamente:
const url = `/busca?${new URLSearchParams({ q: query, pagina: 2 })}`;
Prefira URLSearchParams ao montar queries. É mais difícil de errar, e como ele usa codificação de formulário consistentemente dos dois lados, o problema do mais não aparece.
Três regras que evitam a classe inteira de bug
Codifique valores, não URLs. Codifique cada parâmetro no momento em que o insere. Codificar uma URL montada já é tarde demais — a essa altura você não distingue um & estrutural de um que estava dentro de um valor.
Nunca codifique duas vezes. %20 codificado de novo vira %2520, porque o próprio % é codificado. Codificação dupla aparece como %25 visível na barra de endereço e significa que alguma camada codificou algo que já estava seguro. Codifique em exatamente uma fronteira e deixe viajar.
Decida a questão do mais uma vez, na borda. Escolha codificação de formulário para query strings — é o que os navegadores fazem — e garanta que todo produtor e consumidor da cadeia concorde. Convenções misturadas num mesmo sistema é o que transforma uma ambiguidade em incidente.
Onde mais isso importa
A codificação por porcentagem aparece em mais lugares que query strings, e o conjunto reservado difere um pouco em cada um:
- Segmentos de caminho. Uma
/dentro de um nome de arquivo precisa ser%2Fou cria um nível de diretório que não existe. - Fragmentos. Tudo depois de
#nunca chega ao servidor, então erros de codificação ali são só do cliente — e correspondentemente mais difíceis de notar no log do servidor. - Data URLs. O payload depois da vírgula é codificado por porcentagem, a menos que seja Base64. É frequentemente por isso que uma data URL escrita à mão falha: um
#dentro do SVG encerrou a URL e começou um fragmento. Codificar como%23resolve, e codificar o payload inteiro em Base64 evita a questão por completo.
Dois padrões, uma sintaxe, um caractere de discordância. É um pedacinho de história que vai continuar produzindo bugs enquanto existirem formulários HTML — ou seja, indefinidamente.