encodeURI vs encodeURIComponent: cuál usar y cuándo

encodeURIComponent codifica 11 caracteres que encodeURI deja intactos, y esos 11 caracteres deciden si un valor se queda dentro de su parámetro.

JavaScript tiene dos funciones integradas para la codificación porcentual, y solo se diferencian en un aspecto: cuánta sintaxis de URL dejan intacta. encodeURI supone que recibe una URL completa y conserva los caracteres que le dan estructura, como /, ? y &. encodeURIComponent supone que recibe un único dato y codifica también esos caracteres, para que el dato no pueda confundirse con la estructura. En resumen: para meter un valor en una URL, use encodeURIComponent o, mejor todavía, la API URL.

La diferencia exacta

Las dos funciones convierten a UTF-8 cada carácter que no dejan intacto y escriben cada byte como % seguido de dos dígitos hexadecimales en mayúsculas, de modo que é se convierte en %C3%A9 y € en %E2%82%AC. La única diferencia es el conjunto de caracteres ASCII imprimibles que cada una deja sin tocar:

CaracteresencodeURIencodeURIComponent
A-Z a-z 0-9 - _ . ! ~ * ' ( )IntactosIntactos
# $ & + , / : ; = ? @IntactosCodificados
El espacio y " % < > [ \ ] ^ ` { | }CodificadosCodificados
Caracteres de control y todo lo que queda fuera de ASCIICodificados como bytes UTF-8Codificados como bytes UTF-8

La fila central lo explica todo. Esos 11 caracteres separan las partes de una URL, así que dejarlos intactos dentro de un valor cambia el significado de la URL:

const s = 'Tom & Jerry/2?';
encodeURI(s);           // 'Tom%20&%20Jerry/2?'
encodeURIComponent(s);  // 'Tom%20%26%20Jerry%2F2%3F'

Cuándo usar cada una

Use encodeURIComponent para todo lo que inserte en una URL: el valor o el nombre de un parámetro de consulta, un único segmento de ruta, un fragmento o una URL entera pasada como parámetro, por ejemplo en redirect=. Esto es lo que sale mal con la otra función:

const q = 'rock & roll';

// Wrong: the & starts a new parameter
encodeURI('https://example.com/search?q=' + q);
// 'https://example.com/search?q=rock%20&%20roll'
// The server sees q = 'rock ' and a second parameter ' roll'

// Right
'https://example.com/search?q=' + encodeURIComponent(q);
// 'https://example.com/search?q=rock%20%26%20roll'

encodeURI sirve para el caso concreto en que ya dispone de una URL completa y bien estructurada que solo contiene espacios o caracteres no ASCII, como https://example.com/café?name=Zoë. Incluso entonces esconde dos trampas:

  • Codifica %. Una URL que ya está parcialmente codificada acaba codificada dos veces: encodeURI('https://example.com/a%20b') devuelve https://example.com/a%2520b.
  • Codifica [ y ]. Estos caracteres rodean las direcciones IPv6, así que http://[::1]:8080/ se convierte en http://%5B::1%5D:8080/, que ya no es un host válido.

Si está montando una URL a partir de sus partes, new URL() suele ser la mejor herramienta: codifica cada parte según el lugar en el que aparece.

const url = new URL('https://example.com/search');
url.searchParams.set('q', 'café & thé');
url.href;  // 'https://example.com/search?q=caf%C3%A9+%26+th%C3%A9'

Cómo se comparan con el RFC 3986

El RFC 3986 define dos grupos de caracteres. Los caracteres no reservados (sección 2.3) nunca necesitan codificarse: letras, dígitos y - . _ ~. Los caracteres reservados (sección 2.2) cumplen una función sintáctica: son los delimitadores generales : / ? # [ ] @ y los subdelimitadores ! $ & ' ( ) * + , ; =.

Ninguna de las dos funciones de JavaScript se ajusta exactamente a esto. encodeURIComponent deja sin codificar cinco subdelimitadores: ! ' ( ) *. A los navegadores y a la mayoría de los servidores les da igual, pero no a los sistemas que calculan firmas sobre la URL codificada. Las firmas de OAuth 1.0 (RFC 5849) y AWS Signature Version 4 exigen codificar todos los caracteres salvo los no reservados, así que un valor con un apóstrofo o con paréntesis puede producir una firma que no coincide. La solución habitual:

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

encodeRFC3986("it's (fine)!*");  // 'it%27s%20%28fine%29%21%2A'

URLSearchParams y el signo más

URLSearchParams, y por tanto url.searchParams, no usa ninguna de las dos funciones. Sigue el formato application/x-www-form-urlencoded con el que se envían los formularios HTML, que se diferencia en dos cosas: el espacio se convierte en +, y solo quedan intactos las letras, los dígitos y * - . _, de modo que también se codifican ! ' ( ) ~.

encodeURIComponent('a b+c');                      // 'a%20b%2Bc'
new URLSearchParams({ q: 'a b+c' }).toString();   // 'q=a+b%2Bc'

Los dos resultados son correctos en una query string. Los problemas empiezan cuando el lado que codifica y el que decodifica no siguen la misma convención. decodeURIComponent no convierte + en espacio, así que decodeURIComponent('a+b%2Bc') devuelve a+b+c, mientras que new URLSearchParams('q=a+b%2Bc').get('q') devuelve a b+c. Decodifique las query strings con URLSearchParams, o sustituya + por un espacio antes de llamar a decodeURIComponent. En una ruta, + es simplemente un signo más.

decodeURI vs decodeURIComponent

Las funciones de decodificación son el reflejo de las de codificación. decodeURIComponent decodifica todas las secuencias de escape. decodeURI deja intactas las secuencias de los caracteres que encodeURI no habría codificado, # $ & + , / : ; = ? @, para que decodificar una URL no altere su estructura: decodeURI('%C3%A9%20%2F%3F') devuelve é %2F%3F, mientras que decodeURIComponent devuelve é /?.

Ambas lanzan URIError: URI malformed (la redacción de V8 que usan Chrome y Node.js) cuando un % no va seguido de dos dígitos hexadecimales, como en 100%, o cuando los bytes no son UTF-8 válido, como %E9 procedente de un sistema que codificaba é en Latin-1. Las funciones de codificación lanzan el mismo error ante la mitad suelta de un par suplente (surrogate pair), por ejemplo una cadena cortada en mitad de un emoji.

escape() no es una alternativa

escape() es una función heredada que se conserva por compatibilidad con código antiguo. Deja intactos @ * _ + - . / y no usa UTF-8: los caracteres hasta U+00FF se convierten en un único byte Latin-1, y todo lo que está por encima, en una secuencia %u no estándar. escape('café €') devuelve caf%E9%20%u20AC, que los servidores que esperan UTF-8 no pueden decodificar, y decodeURIComponent('%E9') lanza un error. Sustitúyala por encodeURIComponent, y unescape() por decodeURIComponent, teniendo en cuenta que los datos ya almacenados en el formato antiguo necesitan una conversión.

Doble codificación

Todas las funciones de codificación codifican el propio % como %25, así que aplicar una dos veces produce secuencias como %2520: encodeURIComponent(encodeURIComponent('a b')) devuelve a%2520b. Codifique los valores en bruto exactamente una vez, en el momento en que entran en la URL, y nunca codifique una cadena que ya le llegó codificada. El único caso legítimo es una URL dentro de otra URL: codificar https://example.com/?q=a%20b como parámetro da correctamente ...q%3Da%2520b, y el lado receptor lo decodifica una vez para recuperar la URL interior.

Para comparar los dos modos con su propia entrada, el codificador de URL tiene un selector entre encodeURIComponent y encodeURI. Decodificar URL decodifica mientras escribe y trata + como un espacio, igual que los datos de formulario, así que un valor que contenga un signo más real debería llevarlo como %2B.

Preguntas Frecuentes

¿Qué caracteres no codifica encodeURIComponent?

Las letras, los dígitos y - _ . ! ~ * ' ( ). Todo lo demás se codifica en porcentaje como bytes UTF-8, incluidos / ? & = # y el espacio.

¿Debo usar encodeURI con una URL completa?

Solo cuando la URL ya está bien estructurada y únicamente contiene espacios o caracteres no ASCII. Para construir una URL a partir de sus partes, codifique cada parte con encodeURIComponent o use las API URL y URLSearchParams.

¿Por qué URLSearchParams produce + en lugar de %20?

Usa el formato application/x-www-form-urlencoded de los formularios HTML, en el que + representa un espacio. Los servidores decodifican así las query strings, de modo que las dos formas funcionan en una query string, pero decodeURIComponent por sí sola no vuelve a convertir + en espacio.

¿escape() es lo mismo que encodeURIComponent?

No. escape() es una función heredada que codifica los caracteres no ASCII como Latin-1 o como secuencias %uXXXX en lugar de UTF-8, y deja sin codificar @ + /. Use encodeURIComponent en su lugar.