Codificação de URL: como funciona o percent-encoding
A codificação de URL troca cada byte que não pode aparecer como está em um endereço por um sinal de % seguido do seu valor em hexadecimal.
Uma URL só pode conter um pequeno conjunto de caracteres ASCII, e vários deles têm uma função precisa: / separa os segmentos do caminho, ? abre a query string, & separa os parâmetros e # marca o início do fragmento. Assim que um dado contém um espaço, uma letra acentuada ou um desses caracteres, é preciso codificá-lo. A codificação de URL, também chamada de codificação percentual ou percent-encoding, é definida pela RFC 3986. Na prática, ela diz ao servidor: "este caractere faz parte do valor, não é um separador".
O princípio: um byte vira três caracteres
Um caractere codificado é escrito como % seguido de dois dígitos hexadecimais, que dão o valor de um byte (RFC 3986, seção 2.1). O espaço tem código 32, ou 20 em hexadecimal, e por isso vira %20. Para um texto qualquer, a codificação acontece em três etapas:
- Converter o texto em bytes UTF-8.
- Manter como estão os bytes que correspondem a caracteres não reservados (letras ASCII, dígitos e
- . _ ~). - Trocar cada um dos outros bytes por
%e o seu valor hexadecimal, de preferência em maiúsculas.
Um caractere fora do ASCII ocupa vários bytes em UTF-8, e é por isso que uma única letra gera vários grupos percentuais:
| Caractere | Bytes UTF-8 | Forma codificada |
|---|---|---|
| espaço | 20 | %20 (ou + em um formulário) |
& | 26 | %26 |
= | 3D | %3D |
+ | 2B | %2B |
é | C3 A9 | %C3%A9 |
€ | E2 82 AC | %E2%82%AC |
| 😀 | F0 9F 98 80 | %F0%9F%98%80 |
Assim, "café & thé" vira caf%C3%A9%20%26%20th%C3%A9. A escolha do UTF-8 não é por acaso: a RFC 3986 a recomenda (seção 2.5), e é o que usam os navegadores e as linguagens modernas. Sistemas antigos codificavam o texto em Latin-1, em que é cabe em um único byte e vira %E9. Um decodificador UTF-8 recusa essa sequência: em JavaScript, decodeURIComponent('%E9') lança URIError: URI malformed (URI malformada).
Caracteres reservados e não reservados
A RFC 3986 divide os caracteres em dois grupos:
- Não reservados (seção 2.3):
A-Z a-z 0-9 - . _ ~. Nunca precisam ser codificados, e a norma pede que não sejam:%41eAsignificam a mesma coisa. - Reservados (seção 2.2): os delimitadores gerais
: / ? # [ ] @e os subdelimitadores! $ & ' ( ) * + , ; =. São eles que estruturam a URL.
A regra cabe em uma frase: um caractere reservado é escrito como está quando cumpre seu papel de separador, e codificado quando faz parte de um dado. Compare:
https://exemple.fr/recherche?q=pain&lait -> q = "pain", mais um parâmetro "lait" vazio
https://exemple.fr/recherche?q=pain%26lait -> q = "pain&lait"
Todos os outros caracteres, como espaços, aspas, o próprio %, acentos e emojis, precisam ser codificados em qualquer posição. O sinal % se escreve, portanto, %25: o texto "100 %", com espaço antes do sinal, vira 100%20%25.
O espaço: %20 ou +?
Segundo a RFC 3986, um espaço é codificado como %20. Mas os formulários HTML usam outro formato, application/x-www-form-urlencoded, em que o espaço vira + e um sinal de mais de verdade precisa ser escrito %2B. Os navegadores usam esse formato nos formulários enviados por GET, e a maioria dos servidores decodifica as query strings dessa maneira.
Daí vem uma confusão frequente. Em um caminho, + é um simples sinal de mais. Em uma query string, ele costuma ser lido como espaço: uma busca por "C++" enviada sem codificação, ?q=C++, chega ao servidor como "C" seguido de dois espaços. Codificada corretamente, ela fica ?q=C%2B%2B.
| Linguagem | Espaço como %20 (RFC 3986) | Espaço como + (formulários) |
|---|---|---|
| JavaScript | encodeURIComponent() | URLSearchParams |
| PHP | rawurlencode() | urlencode(), http_build_query() |
| Python | urllib.parse.quote() | quote_plus(), urlencode() |
As duas formas são corretas em uma query string, desde que o lado que decodifica aplique a mesma convenção do lado que codifica.
encodeURI ou encodeURIComponent
O JavaScript oferece duas funções que diferem nos caracteres que deixam intactos. encodeURIComponent codifica tudo, exceto A-Z a-z 0-9 - _ . ! ~ * ' ( ): é a função para usar com um valor, seja um parâmetro ou um segmento de caminho. encodeURI deixa passar também # $ & + , / : ; = ? @, porque parte do princípio de que recebeu uma URL completa com a estrutura já correta. Aplicada a um endereço montado à mão, ela deixa passar os separadores que estão dentro dos dados:
encodeURI('https://exemple.fr/recherche?q=café & thé');
// 'https://exemple.fr/recherche?q=caf%C3%A9%20&%20th%C3%A9'
// o & não é codificado: o valor é cortado em dois
'https://exemple.fr/recherche?q=' + encodeURIComponent('café & thé');
// 'https://exemple.fr/recherche?q=caf%C3%A9%20%26%20th%C3%A9'
O mais seguro continua sendo deixar a API URL codificar cada parte no lugar certo:
const url = new URL('https://exemple.fr/recherche');
url.searchParams.set('q', 'café & thé');
url.searchParams.set('tri', 'prix');
url.href; // 'https://exemple.fr/recherche?q=caf%C3%A9+%26+th%C3%A9&tri=prix'
Exemplos em JavaScript, PHP e Python
Aqui está o mesmo valor codificado e decodificado nas três linguagens, com as duas convenções para o espaço:
// JavaScript
encodeURIComponent('café & thé'); // 'caf%C3%A9%20%26%20th%C3%A9'
new URLSearchParams({ q: 'café & thé' }).toString(); // 'q=caf%C3%A9+%26+th%C3%A9'
decodeURIComponent('caf%C3%A9%20%26%20th%C3%A9'); // 'café & thé'
// PHP (arquivo salvo em UTF-8)
rawurlencode('café & thé'); // caf%C3%A9%20%26%20th%C3%A9
urlencode('café & thé'); // caf%C3%A9+%26+th%C3%A9
http_build_query(['q' => 'café & thé']); // q=caf%C3%A9+%26+th%C3%A9
urldecode('caf%C3%A9+%26+th%C3%A9'); // café & thé
# Python
from urllib.parse import quote, quote_plus, urlencode, unquote_plus
quote('café & thé') # 'caf%C3%A9%20%26%20th%C3%A9'
quote_plus('café & thé') # 'caf%C3%A9+%26+th%C3%A9'
urlencode({'q': 'café & thé'}) # 'q=caf%C3%A9+%26+th%C3%A9'
unquote_plus('caf%C3%A9+%26+th%C3%A9') # 'café & thé'
Dois detalhes que vale conhecer. Em Python, quote() deixa a barra intacta por padrão: quote('a/b') retorna a/b. Para codificar um valor isolado, passe safe='', que dá a%2Fb. Em PHP, urlencode() também codifica o til como %7E, enquanto rawurlencode() o deixa como está, como manda a RFC 3986.
A codificação dupla
Codificar uma string já codificada transforma cada % em %25. %20 vira %2520, e caf%C3%A9 vira caf%25C3%25A9. Na convenção dos formulários, quem denuncia o problema é o +: em PHP, urlencode(urlencode('a b')) retorna a%2Bb, que o servidor decodifica como "a+b" em vez de "a b".
A causa é quase sempre a mesma: duas camadas do código codificam o mesmo valor, por exemplo uma biblioteca HTTP que codifica parâmetros que você já passou codificados. A mesma armadilha existe na decodificação: em PHP, $_GET já traz os valores decodificados, e chamar urldecode() sobre eles decodifica uma segunda vez, o que transforma, entre outras coisas, os + legítimos em espaços. A regra: codifique o valor bruto uma única vez, no momento em que ele entra na URL, e decodifique uma única vez na leitura.
Só um caso justifica uma forma que parece codificação dupla: uma URL passada como parâmetro de outra URL, por exemplo em redirect=. Se a URL interna já contém %20, a sua forma codificada contém %2520, e isso está correto: o destinatário decodifica uma vez e recupera a URL original.
Codificar e decodificar online
Para conferir um valor sem escrever código, o Codificador e Decodificador de URL oferece os dois modos, encodeURIComponent e encodeURI, para comparar o resultado de cada um no seu texto. O Decodificador de URL decodifica enquanto você digita e lê + como espaço, do jeito dos formulários: um sinal de mais de verdade precisa estar escrito como %2B. Tudo acontece no seu navegador.