Codificación URL: reglas de percent-encoding y errores comunes

9 min14 de mayo de 2026

Qué es el percent-encoding y por qué existe

El percent-encoding (también llamado URL encoding) es el mecanismo definido en el RFC 3986 para representar caracteres arbitrarios dentro de una URI. Funciona reemplazando cada byte no permitido por un signo de porcentaje (%) seguido de dos dígitos hexadecimales que representan el valor del byte. Por ejemplo, un espacio se convierte en %20 y la eñe (ñ) en %C3%B1 (sus dos bytes UTF-8).

Las URIs fueron diseñadas con un conjunto limitado de caracteres seguros: letras ASCII (A-Z, a-z), dígitos (0-9), y un puñado de caracteres especiales (- _ . ~). Todo lo demás — incluidos caracteres tan comunes como espacios, acentos, eñes, signos de interrogación fuera del query string, o el símbolo # fuera de un fragmento — debe codificarse para que la URL sea válida.

El motivo histórico es que HTTP y DNS nacieron en un mundo ASCII de 7 bits. Muchos servidores, proxies y routers solo procesaban caracteres ASCII de forma fiable. Aunque hoy casi todo soporta UTF-8, las reglas de percent-encoding siguen vigentes porque forman parte de las especificaciones que definen cómo se parsea una URL. Si envías un carácter no codificado donde se espera uno codificado, la URL puede romperse, truncarse o interpretarse de forma diferente.

Caracteres reservados vs no reservados

El RFC 3986 divide los caracteres en dos categorías claras. Los no reservados (unreserved) nunca necesitan codificación: A-Z a-z 0-9 - _ . ~. Estos son seguros en cualquier parte de una URL.

Los caracteres reservados tienen significado sintáctico dentro de una URL: : / ? # [ ] @ ! $ & ' ( ) * + , ; =. Cada uno tiene un rol específico: ? separa el path del query string, & separa parámetros, # marca un fragmento, / delimita segmentos del path. Si necesitas usar uno de estos caracteres como dato (no como delimitador), debes codificarlo.

La confusión surge porque el mismo carácter puede ser seguro en una parte de la URL y peligroso en otra. La barra / es un delimitador legítimo en el path, pero dentro de un valor de query parameter debe codificarse como %2F. El signo = separa clave de valor en el query string, pero dentro de un valor de parámetro debe ser %3D. El contexto determina todo.

Ejemplo práctico: la URL https://api.ejemplo.com/buscar?q=precio=50€&lang=es tiene dos problemas. El = dentro del valor "precio=50€" se confundirá con el separador clave=valor. Y el € no es ASCII y necesita codificación. La versión correcta es: https://api.ejemplo.com/buscar?q=precio%3D50%E2%82%AC&lang=es.

Caracteres NO reservados (seguros siempre):
A-Z  a-z  0-9  -  _  .  ~

Caracteres RESERVADOS (significado sintáctico):
:  /  ?  #  [  ]  @
!  $  &  '  (  )  *  +  ,  ;  =

Ejemplos de codificación:
Espacio  → %20 (o + en query strings de formularios)
€        → %E2%82%AC (3 bytes UTF-8)
ñ        → %C3%B1 (2 bytes UTF-8)
/        → %2F
?        → %3F
#        → %23
&        → %26
=        → %3D
+        → %2B

encodeURI vs encodeURIComponent: cuándo usar cada una

JavaScript ofrece dos funciones de codificación con comportamientos muy diferentes. encodeURI() está diseñada para codificar una URL completa — preserva los caracteres que tienen significado estructural (: / ? # & = etc.) y solo codifica caracteres no ASCII y algunos inseguros como espacios. Úsala cuando tienes una URL completa y quieres hacerla válida sin destruir su estructura.

encodeURIComponent() codifica TODO excepto los caracteres no reservados (letras, dígitos, - _ . ~). Esto incluye / ? # & = y demás caracteres reservados. Está diseñada para codificar un componente individual de la URL — un valor de parámetro, un segmento de path, etc. Úsala cuando insertas un dato dentro de una URL que ya tiene su estructura.

El error más común es usar encodeURI() para codificar un valor de parámetro. Si el valor contiene & o =, encodeURI() los dejará intactos y romperán la estructura del query string. Ejemplo: encodeURI("a&b=c") devuelve "a&b=c" — parece que tienes dos parámetros cuando solo tienes un valor. encodeURIComponent("a&b=c") devuelve "a%26b%3Dc", que es correcto.

Regla simple: si estás construyendo una URL pieza a pieza, usa encodeURIComponent() para cada valor de parámetro y cada segmento de path que provenga de datos de usuario. Si tienes una URL completa que solo necesita "limpieza" de caracteres no ASCII, usa encodeURI(). En la práctica, encodeURIComponent() es la que usarás el 90% del tiempo.

// encodeURI: para URLs completas
const url = "https://ejemplo.com/ruta con espacios/página.html";
encodeURI(url);
// "https://ejemplo.com/ruta%20con%20espacios/p%C3%A1gina.html"
// ✅ Preserva : / / . — estructura intacta

// encodeURIComponent: para valores individuales
const busqueda = "precio >= 50€ & descuento";
const urlBusqueda = `https://api.com/buscar?q=${encodeURIComponent(busqueda)}`;
// "https://api.com/buscar?q=precio%20%3E%3D%2050%E2%82%AC%20%26%20descuento"
// ✅ Codifica &, =, €, espacios — no rompe el query string

// ❌ Error típico: encodeURI para un valor
const mal = `https://api.com/buscar?q=${encodeURI(busqueda)}`;
// "https://api.com/buscar?q=precio%20>=%2050%E2%82%AC%20&%20descuento"
// ❌ El & no se codificó — el servidor ve dos parámetros

// Construir URL completa de forma segura
const params = new URLSearchParams({
  q: "precio >= 50€",
  lang: "es",
  page: "1"
});
const urlSegura = `https://api.com/buscar?${params.toString()}`;
// URLSearchParams codifica automáticamente cada valor

El caso especial del espacio: %20 vs +

Los espacios tienen dos codificaciones posibles en URLs, lo cual genera confusión constante. El estándar RFC 3986 (URIs genéricas) dicta que un espacio se codifica como %20. Punto. Sin embargo, el estándar HTML para formularios (application/x-www-form-urlencoded, definido en el WHATWG URL Standard) codifica espacios como +.

La razón histórica: el formato application/x-www-form-urlencoded es anterior al RFC 3986 y heredó convenciones de la era temprana de la web. Los formularios HTML con method="GET" envían datos con + para espacios. Los servidores web saben decodificar + como espacio en query strings. Pero + solo es válido como espacio en el query string — en el path de una URL, + es un carácter literal (no un espacio).

En JavaScript, encodeURIComponent() produce %20 para espacios. URLSearchParams produce + para espacios (sigue el estándar de formularios). Ambos son correctos en su contexto. Pero si mezclas formatos — por ejemplo, usas URLSearchParams para construir un query string y luego lo pasas por decodeURIComponent() — el + no se decodificará como espacio. Necesitarías reemplazar + por %20 primero, o usar decodeURIComponent(valor.replace(/\+/g, "%20")).

Mi recomendación: usa %20 siempre que puedas elegir. Es universalmente entendido en todas las partes de una URL. Usa + solo cuando un estándar específico lo requiera (formularios HTML, parámetros OAuth 1.0). Y nunca asumas que + es un espacio en el path — la URL /mi+ruta es literalmente "/mi+ruta", no "/mi ruta".

Codificación de caracteres internacionales (IRI)

Las URLs internacionales con caracteres no ASCII (como https://es.wikipedia.org/wiki/España) se manejan mediante IRIs (Internationalized Resource Identifiers, RFC 3987). Los navegadores muestran la versión legible al usuario pero internamente convierten los caracteres no ASCII a su forma percent-encoded en UTF-8.

El proceso para caracteres no ASCII es: primero se codifican en bytes UTF-8, y luego cada byte se convierte en su representación %XX. La letra ñ en UTF-8 es los bytes C3 B1, así que se codifica como %C3%B1. El emoji 🎉 en UTF-8 es F0 9F 8E 89, codificado como %F0%9F%8E%89. Esto significa que un solo carácter puede ocupar hasta 12 caracteres en una URL (4 bytes UTF-8 × 3 caracteres %XX cada uno).

Los IDN (Internationalized Domain Names) usan un sistema diferente llamado Punycode para el hostname. El dominio españa.es se convierte internamente en xn--espaa-rta.es. Los navegadores muestran la versión Unicode al usuario, pero las peticiones DNS y HTTP usan la forma Punycode. encodeURI() y encodeURIComponent() NO codifican el hostname — solo el path, query y fragment.

Un error frecuente en APIs: el servidor recibe una URL con percent-encoding y la decodifica para procesarla, pero al devolver la URL en una respuesta JSON la incluye sin re-codificar. Si el cliente usa esa URL directamente en otra petición HTTP, los caracteres no ASCII pueden causar problemas según la librería HTTP. La regla: siempre almacena URLs en su forma codificada y solo decodifica para mostrar al usuario.

Errores comunes y cómo evitarlos

Doble codificación: el error más frecuente. Ocurre cuando codificas un valor que ya está codificado. "hola mundo" → %20 se convierte en "hola%20mundo" → que al codificar otra vez se convierte en "hola%2520mundo" (%25 es el percent-encoding de %). El servidor decodifica una vez y obtiene "hola%20mundo" literal en vez de "hola mundo". La solución: codifica exactamente una vez, en el punto donde construyes la URL. Nunca codifiques una URL que recibiste de otra fuente sin verificar si ya está codificada.

Codificación parcial: aplicar encodeURIComponent() a toda una URL en vez de solo a los valores. Esto destruye la estructura: https%3A%2F%2Fejemplo.com%2Fruta ya no es una URL válida. Codifica solo las partes variables, no la estructura fija. O mejor aún, usa la API URL y URLSearchParams que manejan esto automáticamente.

Olvidar codificar el signo +: en muchos frameworks, + en un query string se interpreta como espacio (herencia de formularios HTML). Si tu dato contiene un + literal (como "C++" o una dirección de email con "+"), necesitas codificarlo como %2B. De lo contrario, "[email protected]" se decodificará como "correo [email protected]". URLSearchParams maneja esto correctamente.

No codificar fragmentos de path: si un segmento del path contiene barras, se interpretarán como delimitadores. Para una ruta de archivo como path/to/mi archivo/doc.pdf, cada segmento se codifica por separado: path/to/mi%20archivo/doc.pdf. Si codificas las barras, obtienes path%2Fto%2Fmi%20archivo%2Fdoc.pdf que es un solo segmento de path con barras literales — no lo que quieres.

// ❌ Doble codificación
const valor = "hola mundo";
const yaCodeado = encodeURIComponent(valor); // "hola%20mundo"
const doble = encodeURIComponent(yaCodeado); // "hola%2520mundo" ← MAL

// ✅ Detectar si ya está codificado
function necesitaCodificar(str) {
  try {
    return decodeURIComponent(str) === str;
  } catch {
    return true; // Si falla el decode, no está bien codificado
  }
}

// ✅ Construcción segura con URLSearchParams
const params = new URLSearchParams();
params.set("email", "[email protected]"); // Maneja el + correctamente
params.set("ruta", "docs/archivo final.pdf");
params.set("precio", "50€");
console.log(params.toString());
// "email=usuario%2Btag%40ejemplo.com&ruta=docs%2Farchivo+final.pdf&precio=50%E2%82%AC"

// ✅ Construcción con API URL
const url = new URL("https://api.ejemplo.com/buscar");
url.searchParams.set("q", "C++ & Java");
url.searchParams.set("lang", "es");
console.log(url.toString());
// "https://api.ejemplo.com/buscar?q=C%2B%2B+%26+Java&lang=es"

URL encoding en diferentes contextos

En formularios HTML: cuando un formulario usa method="GET", el navegador codifica los valores con application/x-www-form-urlencoded (espacios como +, caracteres especiales como %XX). Con method="POST" y enctype predeterminado, usa el mismo formato en el body. Con enctype="multipart/form-data", no se aplica percent-encoding — los datos van en formato binario multipart.

En APIs REST: la codificación de parámetros de path y query sigue el RFC 3986 estrictamente. Si tu API acepta un ID que puede contener caracteres especiales (como un email o un path de archivo), codifícalo con encodeURIComponent(). Muchas librerías HTTP (axios, fetch) NO codifican automáticamente los parámetros de path — eres tú quien debe hacerlo.

En headers HTTP: los headers solo permiten un subconjunto de ASCII. Para incluir caracteres no ASCII en headers como Content-Disposition (nombre de archivo para descargas), se usa RFC 5987/8187: filename*=UTF-8''mi%20archivo%20%C3%B1.pdf. El navegador decodifica esto para mostrar "mi archivo ñ.pdf" en el diálogo de descarga.

En cookies: los valores de cookies técnicamente solo permiten un subconjunto de caracteres ASCII (sin espacios, comas, punto y coma). La convención es aplicar encodeURIComponent() al valor al establecer la cookie y decodeURIComponent() al leerla. Algunos frameworks hacen esto automáticamente, otros no — verifica la documentación del tuyo.

Herramientas y mejores prácticas

Usa la API URL de JavaScript siempre que sea posible. new URL() parsea y valida URLs automáticamente, y url.searchParams proporciona una interfaz segura para manipular parámetros sin preocuparse por la codificación manual. Funciona tanto en navegadores modernos como en Node.js.

En backend, la mayoría de frameworks (Express, Django, Rails, Spring) decodifican los parámetros de URL automáticamente antes de entregártelos. No apliques decodeURIComponent() manualmente sobre req.query.valor — ya está decodificado. Aplicar doble decodificación puede convertir datos legítimos que contienen %XX en caracteres incorrectos.

Para depurar problemas de codificación: copia la URL completa en nuestra herramienta de URL encoding/decoding. Decodifícala para ver qué contiene realmente. Si ves %25 seguido de dos dígitos hex, tienes doble codificación. Si ves caracteres no ASCII sin codificar, te falta codificación. Si ves + donde esperabas espacios (o viceversa), tienes un conflicto de formato.

Regla de oro: codifica lo más tarde posible (justo antes de construir la URL final) y decodifica lo más pronto posible (justo después de parsear la URL). Almacena los valores originales sin codificar en tu base de datos y aplicación. La codificación URL es una capa de transporte, no una representación para almacenamiento.