Codificação e decodificação de URL: guia prático de percent encoding para devs
Você está lendo um log de servidor e dá de cara com isso numa query string: %E4%BD%A0%E5%A5%BD. Dados corrompidos? Bug? Nada disso — são os caracteres chineses 你好, convertidos em três bytes UTF-8 cada e depois transformados em percent encoding. Todo dev web passa por isso: algo parece quebrado, mas a URL funciona exatamente como deveria.
Codificação de URL — ou percent encoding — transforma caracteres especiais num formato seguro para URLs. Aqui você vai entender como isso funciona no nível de bytes, quando usar encodeURI versus encodeURIComponent, como codificar em quatro linguagens e quais bugs pegam até quem já tem experiência.
Cole qualquer URL no nosso Decodificador e Codificador de URL para ver a codificação e decodificação em tempo real enquanto acompanha este artigo.
O que é codificação de URL (percent encoding)?
URLs aceitam só um subconjunto pequeno de ASCII. Letras, dígitos e meia dúzia de símbolos passam direto. Todo o resto — espaços, &, texto em chinês, emoji — precisa virar algo que a URL consiga carregar.
Percent encoding troca cada byte inseguro por % seguido de dois dígitos hexadecimais. Espaço vira %20. & vira %26. O nome vem desse prefixo %.
A RFC 3986, de 2005, define as regras e continua valendo. Ela substituiu a RFC 2396 e deixou mais claro quais caracteres são seguros, quais são reservados e como tratar texto não-ASCII.
Exemplos rápidos:
| Entrada | Codificado | Motivo |
|---|---|---|
hello world | hello%20world | Espaço não é permitido em URLs |
price=10&tax=2 | price%3D10%26tax%3D2 | = e & têm significado estrutural |
中 | %E4%B8%AD | Não-ASCII → bytes UTF-8 → percent encoding |
🚀 | %F0%9F%9A%80 | Emoji → 4 bytes UTF-8 → percent encoding |
Quais caracteres precisam de codificação?
A RFC 3986 separa os caracteres em três grupos. Saber quais são poupa horas de debugging.
Caracteres não reservados (nunca codificados)
Esses 66 caracteres passam livres em qualquer parte da URL:
A-Z a-z 0-9 - . _ ~
Só. Letras, dígitos, hífen, ponto, underscore, til. Mais nada.
Caracteres reservados (dependem do contexto)
Esses caracteres funcionam como delimitadores estruturais:
| Caractere | Papel na estrutura da URL |
|---|---|
: | Separa o scheme da authority (https:) |
/ | Separa segmentos do path |
? | Inicia a query string |
# | Inicia o fragmento |
& | Separa parâmetros da query |
= | Separa chave do valor no parâmetro |
@ | Separa userinfo do host |
+ ! $ ' ( ) * , ; [ ] | Diversos papéis reservados |
A regra é simples: se o caractere reservado está cumprindo sua função estrutural, deixe-o. Se aparece como dado (dentro do valor de um parâmetro, por exemplo), codifique.
Todo o resto (sempre codificado)
Espaços, colchetes angulares, chaves, pipes, barras invertidas, qualquer coisa não-ASCII (chinês, árabe, emoji) — tudo precisa de percent encoding antes de entrar numa URL.
Um detalhe sobre espaço: a RFC 3986 codifica como %20, mas formulários HTML usam +. Mais sobre esse conflito adiante.
Como a codificação de URL realmente funciona: o pipeline UTF-8
Com ASCII é direto: pegue o valor do byte em hex e coloque % na frente. Espaço (byte 32, hex 20) vira %20.
Com texto não-ASCII, o processo tem três etapas:
Etapa 1 — Caractere para code point Unicode.
é = U+00E9. 🚀 = U+1F680.
Etapa 2 — Code point para bytes UTF-8.
UTF-8 usa de 1 a 4 bytes conforme a faixa do code point. é (U+00E9) gera dois bytes: 0xC3 0xA9. O foguete (U+1F680) gera quatro: 0xF0 0x9F 0x9A 0x80.
Etapa 3 — Cada byte vira %XX.
Cada byte da etapa anterior ganha seu próprio tripleto percent-encoded.
O pipeline completo para diferentes tipos de caracteres:
| Caractere | Code Point | Bytes UTF-8 | Codificado | Fator de expansão |
|---|---|---|---|---|
A | U+0041 | 41 | A (não codificado) | 1× |
| espaço | U+0020 | 20 | %20 | 3× |
é | U+00E9 | C3 A9 | %C3%A9 | 6× |
中 | U+4E2D | E4 B8 AD | %E4%B8%AD | 9× |
🚀 | U+1F680 | F0 9F 9A 80 | %F0%9F%9A%80 | 12× |
Dá pra confirmar em JavaScript:
const char = '中';
const encoded = encodeURIComponent(char);
console.log(encoded); // '%E4%B8%AD'
// Trace the bytes
const bytes = new TextEncoder().encode(char);
console.log([...bytes].map(b => '%' + b.toString(16).toUpperCase()).join(''));
// '%E4%B8%AD' — matches
Essa expansão pesa nos limites de comprimento de URL. Vinte caracteres chineses viram 180 caracteres de texto percent-encoded.
encodeURI vs encodeURIComponent — escolhendo a função certa
Trocar uma pela outra é o erro de codificação de URL mais comum em JavaScript. Os nomes parecem similares, mas os conjuntos de caracteres que cada uma codifica são bem diferentes.
encodeURI() | encodeURIComponent() | |
|---|---|---|
| Propósito | Codificar uma URL completa | Codificar um único componente (chave ou valor de parâmetro) |
| Preserva | : / ? # & = @ + $ , | Nenhum desses |
| Codifica | Espaços, não-ASCII, alguma pontuação | Tudo exceto A-Z a-z 0-9 - _ . ~ ! ' ( ) * |
| Usar quando | Você tem uma URL completa com espaços ou Unicode no path | Você está montando query parameters a partir de input do usuário |
Um bug que escapa pra produção com uma frequência constrangedora:
// ❌ BUG: encodeURI does NOT encode &
const search = 'Tom & Jerry';
const bad = `https://api.example.com/search?q=${encodeURI(search)}`;
// Result: https://api.example.com/search?q=Tom%20&%20Jerry
// The & splits the query string — server sees q=Tom%20 and a separate param %20Jerry
// ✅ FIX: encodeURIComponent encodes & as %26
const good = `https://api.example.com/search?q=${encodeURIComponent(search)}`;
// Result: https://api.example.com/search?q=Tom%20%26%20Jerry
Na dúvida: encodeURIComponent(). Acerta em 95% dos casos reais.
Teste ambos os modos lado a lado na nossa ferramenta de codificação de URL →
Codificação de URL em cada linguagem
JavaScript (Browser e Node.js)
// Encode a parameter value
const value = encodeURIComponent('price >= 100 & currency = €');
// 'price%20%3E%3D%20100%20%26%20currency%20%3D%20%E2%82%AC'
// Decode
const original = decodeURIComponent(value);
// 'price >= 100 & currency = €'
// Modern approach: URLSearchParams handles encoding automatically
const params = new URLSearchParams({ q: 'hello world', lang: '中文' });
console.log(params.toString());
// 'q=hello+world&lang=%E4%B8%AD%E6%96%87'
// Note: URLSearchParams uses + for spaces (form encoding)
Python
from urllib.parse import quote, unquote, urlencode
# Encode a path segment
quote('hello world/file name.txt', safe='/')
# 'hello%20world/file%20name.txt'
# Encode query parameters
urlencode({'q': '你好', 'page': '1'})
# 'q=%E4%BD%A0%E5%A5%BD&page=1'
# quote_plus uses + for spaces (form encoding)
from urllib.parse import quote_plus
quote_plus('hello world') # 'hello+world'
quote('hello world') # 'hello%20world'
Go
import "net/url"
// Encode a query value (uses + for spaces)
url.QueryEscape("hello world & more")
// "hello+world+%26+more"
// Encode a path segment (uses %20 for spaces)
url.PathEscape("hello world & more")
// "hello%20world%20&%20more"
// Build a URL safely with url.Values
params := url.Values{}
params.Set("q", "你好世界")
params.Set("page", "1")
fmt.Println(params.Encode())
// "page=1&q=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C"
Java
import java.net.URLEncoder;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
// Encode (uses + for spaces — Java follows form encoding)
String encoded = URLEncoder.encode("hello world & more", StandardCharsets.UTF_8);
// "hello+world+%26+more"
// For RFC 3986 compliance, replace + with %20
String rfc3986 = encoded.replace("+", "%20");
// "hello%20world%20%26%20more"
// Decode
String decoded = URLDecoder.decode(encoded, StandardCharsets.UTF_8);
// "hello world & more"
Go e Java usam form encoding por padrão (espaço vira +). Precisa de saída RFC 3986? Substitua + por %20 no resultado.
Cinco bugs de codificação de URL que quebram produção
1. Codificação dupla (%2520 em vez de %20)
Você codifica uma string, ela passa por um framework que codifica de novo, e o % de %20 vira %25. O servidor recebe o texto literal %20 em vez de um espaço.
Sintoma: URLs com %2520, %253D ou outros padrões %25xx.
Diagnóstico: %25 numa URL é sempre suspeito — um % que foi codificado, sinal clássico de codificação dupla.
Correção: Decodifique antes, codifique uma vez. Nunca codifique sem checar se a string já veio codificada.
// Detect double encoding
function isDoubleEncoded(str) {
return /%25[0-9A-Fa-f]{2}/.test(str);
}
// Safe encode: decode first, then encode
function safeEncode(str) {
try { str = decodeURIComponent(str); } catch (e) { /* not encoded, that's fine */ }
return encodeURIComponent(str);
}
2. + em segmentos de path
O dev codifica um nome de arquivo com uma lib que gera + para espaços. my report.pdf vira my+report.pdf. O servidor trata o + como sinal de mais literal — 404.
Regra: + só significa espaço em query strings (depois do ?). No path, + é + mesmo. Use %20 para espaços em paths.
3. URIs de redirect OAuth quebradas
A URL de autorização fica assim:
https://auth.provider.com/authorize?redirect_uri=https://myapp.com/callback?code=abc&state=xyz
O servidor OAuth lê redirect_uri=https://myapp.com/callback?code=abc e interpreta state=xyz como parâmetro de nível superior. Autenticação quebra.
Correção: Codifique a redirect URI inteira:
const redirectUri = 'https://myapp.com/callback?code=abc&state=xyz';
const authUrl = `https://auth.provider.com/authorize?redirect_uri=${encodeURIComponent(redirectUri)}`;
// redirect_uri=https%3A%2F%2Fmyapp.com%2Fcallback%3Fcode%3Dabc%26state%3Dxyz
4. Texto não-ASCII ilegível nos logs
Os logs mostram %E4%BD%A0%E5%A5%BD em vez de caracteres chineses legíveis. Não é bug — a URL está codificada certo. Seu visualizador de logs é que não decodifica percent encoding.
Correção: Passe os logs por um decodificador, ou cole a URL no Decodificador de URL para ler o texto original.
5. Falhas na assinatura de APIs
OAuth 1.0 e AWS Signature V4 exigem codificação estrita conforme a RFC 3986. Só que encodeURIComponent() não codifica !, ', (, ) nem *. Se algum desses aparecer no input da assinatura, ela não bate.
Correção: Pós-processe a saída:
function rfc3986Encode(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, c =>
'%' + c.charCodeAt(0).toString(16).toUpperCase()
);
}
%20 vs + — o dilema da codificação de espaço
Dois padrões, um caractere, confusão infinita.
| Padrão | Espaço vira | Onde se aplica |
|---|---|---|
| RFC 3986 (sintaxe de URI) | %20 | Em qualquer lugar da URL |
application/x-www-form-urlencoded | + | Query strings de submissões de formulários HTML |
O + vem dos primórdios da web. Quando o navegador submete um <form> com method="GET", espaços viram + na query string. Isso está na spec do HTML e não vai mudar.
O problema: + só significa espaço em query strings. No path, + é o sinal de mais literal. https://example.com/my+file.pdf serve um arquivo chamado my+file.pdf, não my file.pdf.
Orientação prática:
- Use
%20ao construir URLs manualmente ou codificar segmentos de path. Funciona em todo lugar. - Aceite
+ao analisar query strings de submissões de formulários — seu framework provavelmente já cuida disso. - Não misture. Escolha uma convenção por componente e siga com ela.
Codificação de URL e segurança
Codificação de URL NÃO é criptografia
Percent encoding é reversível, determinístico e sem chave. Qualquer um decodifica %48%65%6C%6C%6F de volta para Hello em milissegundos.
Não use codificação de URL pra esconder dados. Use HTTPS pra criptografar a requisição inteira. URLs aparecem em logs, histórico do navegador e headers Referer — dados sensíveis vão no corpo da requisição, não na URL.
Ataques de redirecionamento aberto
Atacantes montam URLs codificadas que passam por validações ingênuas. Um redirect com %2F%2Fevil.com decodifica para //evil.com — o navegador trata como URL protocol-relative e manda o usuário pro domínio do atacante.
Defesa: Valide a URL decodificada, nunca a forma codificada. Use allowlist para domínios de redirect.
Exploits de codificação dupla
Um WAF procura tags <script> nas URLs. O atacante manda %253Cscript%253E — o WAF vê percent encoding e libera. A aplicação decodifica uma vez pra %3Cscript%3E, depois uma segunda decodificação produz <script>. Filtro burlado.
Defesa: Normalize todo input (decodifique completamente) antes das verificações de segurança. Um passo só de decodificação não basta.
Para mais sobre fundamentos de segurança web, veja nosso guia Fundamentos de Segurança Web.
Limites de comprimento de URL e quando a codificação fica cara
A spec HTTP não define comprimento máximo de URL, mas cada camada da stack impõe seus limites.
| Camada | Limite |
|---|---|
| Recomendação geral | 2.000 caracteres |
| Chrome, Firefox | ~2 MB (mas servidores rejeitam bem antes) |
| Apache (padrão) | 8.190 bytes |
| Nginx (padrão) | 8.192 bytes |
| IIS | 16.384 bytes (query string) |
| CDNs, proxies | Varia — geralmente 4.096-8.192 bytes |
Percent encoding infla URLs. Um caractere chinês salta de 1 para 9 caracteres (%E4%B8%AD). Emoji vai pra 12. Duzentos caracteres chineses numa query string geram 1.800 caracteres de percent encoding — sem contar a URL base.
Bateu no limite? Migre os dados pra o corpo da requisição com POST. Pra interfaces de busca, um endpoint POST com body JSON costuma resolver.
FAQ
O que é codificação de URL e por que desenvolvedores precisam dela?
Codificação de URL (percent encoding) converte caracteres proibidos em URLs em sequências %XX hexadecimais. URLs aceitam só 66 caracteres ASCII não reservados. Espaços, &, texto Unicode e a maioria da pontuação precisam de codificação pra não quebrar a estrutura da URL nem confundir o servidor.
Qual a diferença entre encodeURI e encodeURIComponent?
encodeURI() codifica uma URL completa, preservando delimitadores como ://, /, ? e &. encodeURIComponent() codifica tudo exceto A-Z a-z 0-9 - _ . ~ ! ' ( ) *. Use encodeURIComponent() pra valores de query parameters. encodeURI() serve quando você já tem a URL completa e quer corrigir espaços ou Unicode sem quebrar a estrutura.
Por que %20 às vezes aparece como + em URLs?
Os dois representam espaço, mas vêm de padrões diferentes. %20 é RFC 3986 e funciona em qualquer parte da URL. + vem da codificação de formulários HTML e só vale em query strings. No path, + é o sinal de mais literal. %20 sempre funciona; + é legado que persiste por causa dos formulários.
Como faço percent encoding em Python, JavaScript, Go e Java?
JavaScript: encodeURIComponent('hello world') gera hello%20world. Python: urllib.parse.quote('hello world') gera hello%20world. Go: url.QueryEscape("hello world") gera hello+world. Java: URLEncoder.encode("hello world", UTF_8) gera hello+world. Go e Java usam form encoding por padrão (espaço = +) — substitua + por %20 pra saída RFC 3986.
Codificação de URL pode ser usada para segurança ou criptografia?
Não. Codificação de URL é reversível sem chave — qualquer pessoa decodifica na hora. Zero confidencialidade. Proteja dados com HTTPS (TLS), não com percent encoding. URLs aparecem em logs, histórico do navegador e headers Referer — dados sensíveis vão no corpo da requisição.
O que é codificação dupla e como resolver?
Acontece quando uma string já codificada passa por codificação de novo. O % de %20 vira %25, produzindo %2520. O servidor recebe o texto literal %20 em vez de um espaço. Solução: decodifique antes, codifique uma vez. %25 seguido de dois dígitos hex na URL é o sinal clássico.
Qual é o comprimento máximo de uma URL?
Não tem máximo oficial na spec HTTP, mas 2.000 caracteres é o limite seguro pra compatibilidade ampla. Apache padrão: 8.190 bytes. Nginx: 8.192 bytes. Caracteres não-ASCII expandem de 3x a 12x com percent encoding, então URLs internacionalizadas estouram o limite mais rápido. Pra payloads grandes, use POST com corpo de requisição.