Fastway
Voltar ao blog

Codificação de URL: por que espaço vira %20 e o + causa bugs

Rodrigo Krohling
·7 min de leitura

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 %2F ou 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 %23 resolve, 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.