Skip to content
Volver al blog
Tutoriales

Codificación de URL: Guía Práctica de Percent Encoding

Reglas RFC 3986, encodeURI vs encodeURIComponent, pipeline UTF-8 y ejemplos en JS, Python, Go y Java. Con herramienta online para probarlo todo.

12 min de lectura

Codificación y decodificación de URL: guía práctica de percent encoding

Estás revisando los logs del servidor y encuentras esto en una query string: %E4%BD%A0%E5%A5%BD. ¿Datos corruptos? ¿Un bug? Nada de eso: son los caracteres chinos 你好, convertidos a tres bytes UTF-8 cada uno y luego percent-encoded para caber en una URL. Algo parece roto, pero la URL funciona tal cual fue diseñada.

La codificación URL — o percent encoding — hace seguros los caracteres especiales dentro de las URLs. Esta guía cubre cómo funciona a nivel de bytes, cuándo usar encodeURI frente a encodeURIComponent, cómo codificar en cuatro lenguajes y los errores que atrapan hasta a desarrolladores con experiencia.

Pega cualquier URL en nuestro Decodificador y Codificador de URL para ver la codificación y decodificación en tiempo real mientras sigues esta guía.

¿Qué es la codificación URL (Percent Encoding)?

Una URL solo admite un subconjunto reducido de caracteres ASCII. Letras, dígitos y un puñado de símbolos viajan sin problema. Todo lo demás — espacios, ampersands, texto en chino, emojis — necesita convertirse a un formato que las URLs puedan transportar.

El percent encoding reemplaza cada byte no seguro con un signo % seguido de dos dígitos hexadecimales. Un espacio se convierte en %20. Un ampersand se convierte en %26. El nombre viene de ese prefijo %.

Las reglas vienen del RFC 3986, publicado en 2005 y vigente hoy. Reemplazó al RFC 2396 y redefinió qué caracteres son seguros, cuáles reservados y cómo manejar texto no ASCII.

Ejemplos rápidos:

EntradaCodificadoPor qué
hello worldhello%20worldLos espacios no están permitidos en URLs
price=10&tax=2price%3D10%26tax%3D2= y & tienen significado estructural
%E4%B8%ADNo ASCII → bytes UTF-8 → percent encoding
🚀%F0%9F%9A%80Emoji → 4 bytes UTF-8 → percent encoding

¿Qué caracteres necesitan codificación?

RFC 3986 divide los caracteres en tres grupos. Conocerlos te ahorra horas de depuración.

Caracteres no reservados (nunca se codifican)

Estos 66 caracteres pasan tal cual en cualquier parte de una URL:

A-Z  a-z  0-9  -  .  _  ~

Solo esos. Letras, dígitos, guion, punto, guion bajo, virgulilla. Nada más pasa sin codificar.

Caracteres reservados (dependen del contexto)

Estos caracteres sirven como delimitadores estructurales en las URLs:

CarácterRol en la estructura de la URL
:Separa el esquema de la autoridad (https:)
/Separa segmentos del path
?Inicia la query string
#Inicia el fragmento
&Separa parámetros de query
=Separa clave de valor en un parámetro
@Separa userinfo del host
+ ! $ ' ( ) * , ; [ ]Otros roles reservados

La regla es simple: si el carácter reservado cumple su rol estructural, se deja tal cual. Si aparece como dato (dentro del valor de un parámetro, por ejemplo), se codifica.

Todo lo demás (siempre se codifica)

Espacios, corchetes angulares, llaves, pipes, barras invertidas, caracteres no ASCII (chino, árabe, emojis) — todos deben codificarse con percent encoding antes de aparecer en una URL.

Caso especial: el espacio. RFC 3986 lo codifica como %20, pero los formularios HTML usan +. Más sobre este lío después.

Cómo funciona realmente la codificación URL: el pipeline UTF-8

Con caracteres ASCII es directo: tomas el valor del byte en hexadecimal y le antepones %. Un espacio (byte 32, hex 20) se convierte en %20.

Con texto no ASCII, hay tres pasos:

Paso 1 — Carácter a code point Unicode. El carácter é corresponde al code point U+00E9. El emoji 🚀 corresponde a U+1F680.

Paso 2 — Code point a bytes UTF-8. UTF-8 usa de 1 a 4 bytes según el rango del code point. é (U+00E9) son dos bytes: 0xC3 0xA9. El emoji del cohete (U+1F680) son cuatro: 0xF0 0x9F 0x9A 0x80.

Paso 3 — Cada byte a %XX. Cada byte del paso 2 recibe su propio triplete percent-encoded.

El pipeline completo para varios tipos de caracteres:

CarácterCode PointBytes UTF-8CodificadoFactor de expansión
AU+004141A (no se codifica)
espacioU+002020%20
éU+00E9C3 A9%C3%A9
U+4E2DE4 B8 AD%E4%B8%AD
🚀U+1F680F0 9F 9A 80%F0%9F%9A%8012×

Verifícalo en 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

Esta expansión importa cuando te acercas a los límites de longitud de URL. Con 20 caracteres chinos ya sumas 180 caracteres de texto percent-encoded.

encodeURI vs encodeURIComponent — Elegir la función correcta

Confundir estas dos funciones es el error de codificación URL más frecuente en JavaScript. Se parecen, pero codifican conjuntos de caracteres distintos.

encodeURI()encodeURIComponent()
PropósitoCodificar una URL completaCodificar un solo componente (clave o valor de parámetro)
Preserva: / ? # & = @ + $ ,Ninguno de estos
CodificaEspacios, no ASCII, algo de puntuaciónTodo excepto A-Z a-z 0-9 - _ . ~ ! ' ( ) *
Usar cuandoTienes una URL completa con espacios o Unicode en el pathEstás construyendo parámetros de query con entrada del usuario

Un bug que llega a producción con más frecuencia de la que nadie admite:

// ❌ 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

Regla rápida: en caso de duda, usa encodeURIComponent(). Es la opción correcta en el 95% de los casos.

Prueba ambos modos lado a lado en nuestra herramienta URL Encoder →

Codificación URL en cada lenguaje

JavaScript (Browser y 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"

Ojo con Go y Java: ambos usan codificación de formularios por defecto (espacios como +). Para salida RFC 3986, reemplaza + con %20 en el resultado.

Cinco bugs de codificación URL que rompen producción

1. Doble codificación (%2520 en lugar de %20)

Codificas una cadena, la pasas por un framework que la codifica otra vez, y el % en %20 se convierte en %25. El servidor acaba viendo el texto literal %20 en vez de un espacio.

Síntoma: Las URLs contienen %2520, %253D u otros patrones %25xx.

Diagnóstico: Si ves %25 en una URL, sospecha. Significa que un % fue codificado, señal casi segura de doble codificación.

Solución: Decodifica primero, codifica una sola vez. Antes de codificar, verifica que la cadena no venga ya 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. + en segmentos del path

Codificas un nombre de archivo con una librería que genera + para los espacios. El archivo my report.pdf queda como my+report.pdf. El servidor trata el + como literal y devuelve un 404.

La regla: + significa espacio solo en query strings (después de ?). En segmentos del path, + es simplemente +. Usa siempre %20 para espacios en paths.

3. URIs de redirección OAuth rotas

La URL de autorización se ve así:

https://auth.provider.com/authorize?redirect_uri=https://myapp.com/callback?code=abc&state=xyz

El servidor OAuth interpreta redirect_uri=https://myapp.com/callback?code=abc y trata state=xyz como parámetro de nivel superior. La autenticación falla.

Solución: Codifica el valor completo del redirect URI:

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 no ASCII ilegible en logs

Los logs del servidor muestran %E4%BD%A0%E5%A5%BD en lugar de caracteres chinos legibles. No es un bug — la URL está bien codificada. Tu visor de logs simplemente no decodifica las secuencias percent-encoded.

Solución: Pasa los logs por un decodificador, o pega la URL en un Decodificador de URL para leer el texto original.

5. Fallos en la firma de APIs

OAuth 1.0 y AWS Signature V4 exigen codificación estricta RFC 3986. Pero encodeURIComponent() en JavaScript no codifica !, ', (, ) ni *. Si alguno aparece en tu input de firma, la firma no va a coincidir.

Solución: Post-procesa la salida:

function rfc3986Encode(str) {
  return encodeURIComponent(str).replace(/[!'()*]/g, c =>
    '%' + c.charCodeAt(0).toString(16).toUpperCase()
  );
}

%20 vs + — El dilema de la codificación del espacio

Dos estándares, un carácter, confusión interminable.

EstándarEl espacio se convierte enDónde aplica
RFC 3986 (sintaxis de URI)%20En toda la URL
application/x-www-form-urlencoded+Query strings de envíos de formularios HTML

La convención del + viene de los primeros días de la web. Cuando un navegador envía un <form> HTML con method="GET", codifica los espacios como + en la query string. Está así en la spec de HTML y no va a desaparecer.

El problema: + solo significa “espacio” en query strings. En el path, + es un signo de más literal. https://example.com/my+file.pdf sirve un archivo llamado my+file.pdf, no my file.pdf.

Guía práctica:

  • Usa %20 cuando construyas URLs manualmente o codifiques segmentos del path. Funciona en todas partes.
  • Acepta + al parsear query strings de envíos de formularios — tu framework probablemente ya lo maneja.
  • No los mezcles. Elige una convención por componente y mantenla.

Codificación URL y seguridad

La codificación URL NO es cifrado

Percent encoding es una transformación reversible y determinista. Sin clave, sin secreto, cero seguridad. Cualquiera decodifica %48%65%6C%6C%6F de vuelta a Hello en milisegundos.

No uses codificación URL para ocultar datos sensibles. Usa HTTPS para cifrar la petición. Las URLs aparecen en logs, historial del navegador y cabeceras Referer — la información sensible va en el cuerpo de la petición, no en la URL.

Ataques de redirección abierta

Los atacantes codifican URLs para evadir validaciones ingenuas. Un parámetro de redirección con %2F%2Fevil.com se decodifica a //evil.com, y el navegador lo interpreta como URL relativa al protocolo que apunta al dominio del atacante.

Defensa: Valida siempre la URL decodificada, no la forma codificada. Usa listas blancas para los dominios de redirección.

Exploits de doble codificación

Un WAF revisa las URLs buscando etiquetas <script>. El atacante envía %253Cscript%253E — el WAF ve texto percent-encoded inofensivo. La aplicación lo decodifica una vez a %3Cscript%3E, una segunda pasada produce <script>, y el filtro queda burlado.

Defensa: Normaliza toda la entrada (decodifica completamente) antes de aplicar controles de seguridad. No confíes en una sola pasada de decodificación.

Si quieres profundizar, mira nuestra guía de Fundamentos de Seguridad Web.

Límites de longitud de URL y cuándo la codificación se vuelve costosa

La spec HTTP no define una longitud máxima de URL. En la práctica, cada capa de la pila impone la suya.

CapaLímite
Recomendación general2,000 caracteres
Chrome, Firefox~2 MB (pero los servidores rechazan mucho antes)
Apache (por defecto)8,190 bytes
Nginx (por defecto)8,192 bytes
IIS16,384 bytes (query string)
CDNs, proxiesVaría — frecuentemente 4,096-8,192 bytes

El percent encoding alarga las URLs rápido. Un carácter chino pasa de 1 a 9 caracteres (%E4%B8%AD). Un emoji, a 12. Con 200 caracteres chinos en la query string ya suman 1,800 caracteres de percent encoding antes de la URL base.

Cuando llegas al límite: Mueve los datos al cuerpo de la petición con POST. Para búsquedas, un endpoint POST que acepte JSON con los criterios resuelve el problema.

FAQ

¿Qué es la codificación URL y por qué la necesitan los desarrolladores?

La codificación URL (percent encoding) convierte los caracteres no permitidos en URLs a secuencias hexadecimales %XX. Las URLs solo admiten 66 caracteres ASCII no reservados. Espacios, ampersands, texto Unicode y la mayoría de la puntuación se codifican para no romper la estructura de la URL ni causar interpretaciones erróneas en el servidor.

¿Cuál es la diferencia entre encodeURI y encodeURIComponent?

encodeURI() codifica una URL completa preservando caracteres estructurales como ://, /, ? y &. encodeURIComponent() codifica todo excepto A-Z a-z 0-9 - _ . ~ ! ' ( ) *. Usa encodeURIComponent() para valores de parámetros de query. Usa encodeURI() solo cuando tengas una URL completa y quieras corregir espacios o caracteres no ASCII sin romper su estructura.

¿Por qué %20 aparece a veces como + en las URLs?

Ambos representan un espacio, pero vienen de estándares distintos. %20 sigue RFC 3986 y funciona en toda la URL. + viene de la codificación de formularios HTML y solo funciona en query strings. En el path, + es un signo de más literal. %20 siempre es seguro; + es una convención heredada de los formularios HTML.

¿Cómo codifico texto en URL con Python, JavaScript, Go y Java?

JavaScript: encodeURIComponent('hello world')hello%20world. Python: urllib.parse.quote('hello world')hello%20world. Go: url.QueryEscape("hello world")hello+world. Java: URLEncoder.encode("hello world", UTF_8)hello+world. Go y Java usan por defecto codificación de formularios (espacio como +) — reemplaza + con %20 para salida conforme a RFC 3986.

¿Se puede usar la codificación URL para seguridad o cifrado?

No. La codificación URL es reversible sin clave — cualquiera la decodifica al instante. Cero confidencialidad. Para datos sensibles usa HTTPS (cifrado TLS), no percent encoding. Las URLs aparecen en logs, historial del navegador y cabeceras Referer: los datos sensibles van en el cuerpo de la petición, no en la URL.

¿Qué es la doble codificación y cómo se soluciona?

La doble codificación ocurre cuando una cadena ya codificada se codifica otra vez. El % en %20 se convierte en %25, produciendo %2520. El servidor ve %20 literal en vez de un espacio. Solución: decodifica primero, codifica una sola vez. El patrón %25 seguido de dos hex digits delata el problema.

¿Cuál es la longitud máxima de una URL?

No hay máximo oficial en la spec HTTP, pero 2,000 caracteres es el límite práctico para compatibilidad general. Apache por defecto acepta 8,190 bytes, Nginx 8,192. Los caracteres no ASCII se expanden entre 3x y 12x con percent encoding, así que las URLs internacionalizadas chocan con el límite mucho antes. Para payloads grandes, usa POST con cuerpo de petición.

Artículos relacionados

Ver todos los artículos