Codificación
Reglas de codificación URL — encodeURI vs encodeURIComponent
La codificación URL es simple hasta que construyes URLs con input del usuario. Las reglas del percent-encoding, las dos funciones de JavaScript y los bordes que sí importan.
La codificación URL es uno de esos temas que parecen triviales hasta que el tráfico de producción demuestra lo contrario. Todo equipo con el que he trabajado ha tenido al menos una caída por un encodeURIComponent ausente o duplicado. Esta guía es el modelo mental completo, más las trampas específicas de JavaScript.
La regla de una línea
Si estás insertando un valor dinámico en una URL — query param, segmento de path, fragmento — pásalo por encodeURIComponent. Si te dan una URL completa y necesitas hacerla segura para transmitir, pásala por encodeURI. La mayoría de bugs vienen de usar la equivocada.
Qué es el percent-encoding
RFC 3986 define dos categorías de caracteres: reservados y no reservados.
Los no reservados nunca requieren encoding: A-Z a-z 0-9 - _ . ~. Todo lo demás o es reservado (tiene significado estructural en URLs: / ? # [ ] @ ! $ & ' ( ) * + , ; =) o está fuera de ASCII (requiere encoding porque las URLs son ASCII por spec).
El percent-encoding sustituye un byte por %XX donde XX es su valor hex. space (0x20) se convierte en %20. é en UTF-8 son dos bytes 0xC3 0xA9 y encodean a %C3%A9. Sí — el URL encoding es UTF-8 por convención moderna. El URL encoding en Latin-1 es un cuento de fantasmas de los 90.
| Carácter | Categoría | Encodeado |
|---|---|---|
a-z, A-Z, 0-9 | No reservado | nunca |
-, _, ., ~ | No reservado | nunca |
!, *, ', (, ) | Sub-delims (reservado) | a veces |
/, ?, #, [, ] | Gen-delims (reservado) | en valores de query/path |
| espacio | Otro | %20 en paths, + en form data |
é (U+00E9) | No ASCII | %C3%A9 en UTF-8 |
La fila de “a veces” es donde las dos funciones de JavaScript discrepan.
encodeURI vs encodeURIComponent
JavaScript (y casi todos los lenguajes) te dan dos funciones. Se diferencian en exactamente una cosa: qué caracteres reservados se encodean.
encodeURIdeja tranquilos los caracteres estructurales::/?#[]@!$&'()*+,;=. Está pensado para llamarse sobre una URL completa donde esos caracteres aún llevan su significado estructural.encodeURIComponentencodea casi todo excepto el conjunto no reservado. Está pensado para llamarse sobre un fragmento suelto (un valor de query, un segmento de path) que NO debe tener significado estructural.
const value = "a/b?c=d&e f";
encodeURI(value);
// "a/b?c=d&e%20f" -- / ? = & sobreviven, espacio encodeado
encodeURIComponent(value);
// "a%2Fb%3Fc%3Dd%26e%20f" -- todo lo inseguro encodeado
La llamada correcta depende de si el string es URL completa o fragmento:
// URL completa (probablemente tecleada por usuario, o plantilla URL ya)
const url = "https://example.com/search?q=hello world";
fetch(encodeURI(url));
// → https://example.com/search?q=hello%20world
// Construyendo query string
const query = "cats & dogs";
const url2 = `https://example.com/search?q=${encodeURIComponent(query)}`;
// → https://example.com/search?q=cats%20%26%20dogs
El segundo caso es el que los equipos lían. Usar encodeURI(query) dejaría el & intacto y el servidor interpretaría tu query como dos parámetros.
Segmento de path vs query — sutil pero real
La buena práctica moderna es usar URL y URLSearchParams en vez de concatenar a mano:
const url = new URL("https://example.com/search");
url.searchParams.set("q", "cats & dogs");
url.searchParams.set("since", "2026-04-17");
url.toString();
// "https://example.com/search?q=cats+%26+dogs&since=2026-04-17"
Fíjate en que URLSearchParams encodea el espacio como +, no %20. Porque implementa application/x-www-form-urlencoded (el encoding de formularios HTML), no el encoding de path RFC 3986. Ambas son válidas en query strings, y los servidores manejan las dos. Pero si construyes una porción de URL que no es query, quédate con %20.
Para segmentos de path no hay equivalente URLPathParams. Constrúyelos a mano con encodeURIComponent:
const id = "café/au/lait";
const url = `/items/${encodeURIComponent(id)}`;
// → /items/caf%C3%A9%2Fau%2Flait
Si quieres slugs legibles en lugar de valores percent-encoded, necesitas otra herramienta — el generador de slug convierte texto arbitrario en kebab-case URL-safe.
La trampa del doble encoding
Ejecutar encodeURIComponent dos veces no es idempotente.
encodeURIComponent("hello world");
// "hello%20world"
encodeURIComponent("hello%20world");
// "hello%2520world" -- el propio % se encodeó
Esto muerde cuando un valor pasa por dos capas que ambas creen ser dueñas del encoding. Casos clásicos: una URL de redirect embebida como query parameter, o un submit de formulario que ya encodeó sus valores y luego el framework los encodea otra vez.
Regla: encodea una vez, en el borde donde construyes la URL. No encodees en el origen de los datos. Decodea al recibir. Si tu valor llega ya encodeado a tu código, decodéalo antes de construir una URL nueva.
Form data — la sorpresa del +
Los formularios HTML encodean bodies distinto a los paths de URL. Un submit con hello world envía hello+world, no hello%20world. Ambos decodean a los mismos bytes en un servidor correcto, pero si lees un body de form crudo con un replace de strings o regex, tienes que manejar + → espacio.
function decodeForm(str) {
return decodeURIComponent(str.replace(/\+/g, "%20"));
}
El constructor URLSearchParams hace esto por ti automáticamente cuando le pasas document.body o un string application/x-www-form-urlencoded.
Unicode — confía en UTF-8
Tres hechos que cubren el 95% de bugs reales:
- Las URLs modernas son UTF-8. El navegador encodea
éen un query string como%C3%A9. Tu servidor debe decodear%C3%A9como bytes UTF-8, no Latin-1. - Los IRIs (Internationalized Resource Identifiers) te dejan teclear
https://example.com/caféen la barra. El navegador convierte ahttps://example.com/caf%C3%A9antes de enviar. Tu servidor recibe la forma encodeada. - Los hostnames usan otro encoding (Punycode) para nombres de dominio no-ASCII.
café.comse vuelvexn--caf-dma.com. El percent-encoding no aplica a hostnames — nunca.
Regex y URL encoding juntos
Si construyes patrones regex contra datos URL-encoded, recuerda que las secuencias percent son tres caracteres (% + dos hex). Puedes matchear caracteres encodeados con %[0-9A-Fa-f]{2}. Para el resto de regex, el cheatsheet de regex tiene los operadores y flags.
Para el encoding Base64 vecino que surge en conversaciones similares (JWTs, data URIs, binarios embebidos), mira la guía de Base64 en producción.
Conclusiones
encodeURIComponent para fragmentos (valores de query, segmentos de path). encodeURI para URLs enteras con caracteres estructurales. Usa URL y URLSearchParams cuando puedas — gestionan los bordes. Encodea una vez, en el borde. Confía en UTF-8 para Unicode. Cuando veas %2520 en un log, has encodeado dos veces.