5 errores comunes de descodificación de URL y cómo solucionarlos

Los errores de descodificación de URL pueden convertir una experiencia de usuario fluida en una pesadilla de depuración. Basándonos en años de experiencia en desarrollo web y miles de informes de errores, estos son los 5 errores más comunes de descodificación de URL y exactamente cómo solucionarlos.

Error #1: Formato de codificación porcentual incorrecto

El problema

No todas las cadenas que parecen codificadas en URL son realmente válidas. Las secuencias porcentuales inválidas harán que la descodificación falle.

Patrones inválidos comunes:

hello%world      // Faltan dígitos hexadecimales
test%2          // Secuencia incompleta (necesita 2 dígitos hex)
data%ZZ         // Caracteres hexadecimales inválidos
url%GG%20test   // Mezcla de inválidos (%GG) y válidos (%20)

Qué ocurre

// ¡Esto lanzará un error!
decodeURIComponent('hello%world');
// URIError: URI mal formada

decodeURIComponent('test%2');
// URIError: URI mal formada

La causa raíz

  1. Construcción manual de URL sin codificación adecuada
  2. URLs truncadas (errores de copiar y pegar)
  3. Datos no-URL confundidos con cadenas codificadas
  4. Sistemas heredados que no siguen RFC 3986

La solución

Solución #1: Validar antes de descodificar

function esValidoCodificado(str) {
  // Buscar patrones porcentuales inválidos
  const patronInvalido = /%(?![0-9A-Fa-f]{2})|%[0-9A-Fa-f](?![0-9A-Fa-f])/;
  
  if (patronInvalido.test(str)) {
    return false;
  }
  
  // Intentar descodificar - si lanza error, es inválido
  try {
    decodeURIComponent(str);
    return true;
  } catch (e) {
    return false;
  }
}

// Uso
const entradaUsuario = params.get('search');
if (esValidoCodificado(entradaUsuario)) {
  const descodificado = decodeURIComponent(entradaUsuario);
} else {
  console.error('Codificación URL inválida detectada');
  // Manejar el error apropiadamente
}

Solución #2: Sanear codificaciones malformadas

function sanearCodificacion(str) {
  // Reemplazar secuencias porcentuales incompletas o inválidas
  return str.replace(/%(?![0-9A-Fa-f]{2})/g, '%25');
  // Convierte % a %25 cuando no está seguido de 2 dígitos hex
}

// Ejemplo
sanearCodificacion('hello%world');  // → 'hello%25world'
decodeURIComponent(sanearCodificacion('hello%world'));  // → 'hello%world'

Solución #3: Preprocesar con regex

function descodificarSeguro(str) {
  try {
    return decodeURIComponent(str);
  } catch (e) {
    // Respaldo: reemplazar manualmente patrones comunes
    return str
      .replace(/%20/g, ' ')
      .replace(/%21/g, '!')
      .replace(/%40/g, '@')
      .replace(/%23/g, '#')
      .replace(/%25/g, '%');
    // Nota: Esto no es exhaustivo, solo un respaldo
  }
}

Prevención

Use siempre funciones de codificación adecuadas:

// ✅ Correcto
const consulta = encodeURIComponent(entradaUsuario);
const url = `/search?q=${consulta}`;

// ❌ Incorrecto - construcción manual de URL
const url = `/search?q=${entradaUsuario.replace(/ /g, '%20')}`;

Prueba rápida

// Casos de prueba para validación
const casosPrueba = [
  { input: 'hello%20world', valid: true },
  { input: 'hello%world', valid: false },
  { input: 'test%2', valid: false },
  { input: '%E4%B8%AD%E6%96%87', valid: true },
  { input: 'normal-text', valid: true },  // Sin codificación también es válido
];

casosPrueba.forEach(({ input, valid }) => {
  const resultado = esValidoCodificado(input);
  console.assert(resultado === valid, `Falló para: ${input}`);
});

Error #2: Discrepancias de codificación de caracteres

El problema

Codificar una cadena en un conjunto de caracteres (ej., ISO-8859-1) y descodificarla como otro (UTF-8) produce texto sin sentido o el carácter de reemplazo �.

Síntomas:

Esperado: café
Obtenido: café

Esperado: 中文
Obtenido: ���

Esperado: Ñoño
Obtenido: �o�o

Qué ocurre

// Si el servidor codificó en ISO-8859-1 pero descodificas como UTF-8:
const codificado = '%C3%A9';  // é en UTF-8
decodeURIComponent(codificado);  // → 'é' (correcto en UTF-8)

// Pero si fue realmente codificado en ISO-8859-1 como %E9:
const codificacionIncorrecta = '%E9';
decodeURIComponent(codificacionIncorrecta);  // → 'é' pero se muestra mal

La causa raíz

  1. Sistemas heredados usando codificaciones no UTF-8
  2. Codificación mixta en diferentes partes de la aplicación
  3. Base de datos configurada con charset incorrecto
  4. Cabeceras HTTP especificando codificación incorrecta

La solución

Solución #1: Estandarizar en UTF-8 en todas partes

<!-- En HTML -->
<meta charset="UTF-8">

<!-- En cabeceras HTTP -->
Content-Type: text/html; charset=UTF-8
// En Express.js
app.use(express.urlencoded({ extended: true, charset: 'utf-8' }));
-- En MySQL
CREATE DATABASE mibd CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

Solución #2: Detectar discrepancias de codificación

function pareceTextoCorrupto(str) {
  // Patrones comunes de UTF-8 interpretado como ISO-8859-1
  const patronesSospechosos = [
    /é|è|à |ç/,  // Común en francés
    /£|Â¥|©/,      // Monedas y símbolos
    /�/,            // Carácter de reemplazo
  ];
  
  return patronesSospechosos.some(patron => patron.test(str));
}

// Uso
const descodificado = decodeURIComponent(codificado);
if (pareceTextoCorrupto(descodificado)) {
  console.warn('¡Posible discrepancia de codificación detectada!');
}

Solución #3: Recodificar si es necesario

// Si sabes que la fuente era Latin-1 pero se descodificó como UTF-8:
function arreglarLatin1AUTf8(str) {
  // Esta es una operación compleja, usa una biblioteca si es posible
  const codificador = new TextEncoder();
  const descodificador = new TextDecoder('iso-8859-1');
  
  const bytes = codificador.encode(str);
  return descodificador.decode(bytes);
}

Prevención

Imponer UTF-8 en todas las capas:

  1. Base de datos: UTF-8 (o utf8mb4 para MySQL)
  2. Cabeceras HTTP: Content-Type: charset=UTF-8
  3. HTML: <meta charset="UTF-8">
  4. Archivos fuente: Guardar como UTF-8
  5. APIs: Aceptar y devolver UTF-8

Prueba rápida

// Probar con caracteres internacionales
const pruebas = [
  { text: 'café', lang: 'Francés' },
  { text: '中文', lang: 'Chino' },
  { text: 'العربية', lang: 'Árabe' },
  { text: '😀', lang: 'Emoji' },
];

pruebas.forEach(({ text, lang }) => {
  const codificado = encodeURIComponent(text);
  const descodificado = decodeURIComponent(codificado);
  console.assert(descodificado === text, `Codificación ${lang} falló`);
});

Error #3: Descodificación incompleta (problemas de múltiples capas)

El problema

Las URLs codificadas múltiples veces necesitan múltiples operaciones de descodificación. Detenerse demasiado pronto deja secuencias porcentuales en la salida.

Ejemplo:

Original:      Hello World
Codificado 1x:  Hello%20World  
Codificado 2x: Hello%2520World
Codificado 3x: Hello%252520World

// Si solo descodificas una vez:
decodeURIComponent('Hello%252520World')  // → 'Hello%2520World' (¡aún codificado!)

Qué ocurre

const dobleCodificado = 'search%253Dhello%2520world';

// Descodificar una vez
const unavez = decodeURIComponent(dobleCodificado);
console.log(unavez);  // 'search%3Dhello%20world' - ¡aún contiene %3D y %20!

// Descodificar dos veces
const dosveces = decodeURIComponent(unavez);
console.log(dosveces);  // 'search=hello world' - ¡correcto!

La causa raíz

  1. Múltiples redireccionamientos cada uno codificando la URL
  2. Cadenas de middleware que codifican repetidamente
  3. Copiar-pegar del usuario de URLs ya codificadas
  4. Auto-codificación del framework encima de codificación manual

La solución

Solución #1: Descodificación iterativa hasta estabilizar

function descodificarCompletamente(str) {
  let descodificado = str;
  let anterior = '';
  let iteraciones = 0;
  const MAX_ITERACIONES = 5;  // Límite de seguridad
  
  while (descodificado !== anterior && iteraciones < MAX_ITERACIONES) {
    anterior = descodificado;
    try {
      const temp = decodeURIComponent(descodificado);
      // Solo continuar si algo realmente cambió
      if (temp !== descodificado) {
        descodificado = temp;
      } else {
        break;
      }
    } catch (e) {
      // Detener en error
      console.error('Descodificación detenida por error:', e);
      break;
    }
    iteraciones++;
  }
  
  console.log(`Descodificado ${iteraciones} veces`);
  return descodificado;
}

// Uso
descodificarCompletamente('Hello%252520World');  // → 'Hello World' (3 iteraciones)

Solución #2: Contar capas de codificación

function contarCapas(str) {
  let cantidad = 0;
  let actual = str;
  
  while (/%[0-9A-Fa-f]{2}/.test(actual) && cantidad < 10) {
    try {
      const descodificado = decodeURIComponent(actual);
      if (descodificado === actual) break;  // Sin cambios
      actual = descodificado;
      cantidad++;
    } catch (e) {
      break;
    }
  }
  
  return cantidad;
}

// Uso
console.log(contarCapas('Hello%20World'));       // 1
console.log(contarCapas('Hello%2520World'));     // 2
console.log(contarCapas('Hello%252520World'));   // 3

Solución #3: Detectar y advertir

function descodificarConAdvertencia(str) {
  const capas = contarCapas(str);
  
  if (capas > 1) {
    console.warn(`Codificación de múltiples capas detectada: ${capas} capas`);
  }
  
  return descodificarCompletamente(str);
}

Prevención

Evitar doble codificación:

// ❌ No hagas esto
const yaCodificado = encodeURIComponent(entradaUsuario);
const dobleCodificado = encodeURIComponent(yaCodificado);  // ¡Mal!

// ✅ Codifica solo una vez
const codificado = encodeURIComponent(entradaUsuario);

// ✅ O verifica si ya está codificado
function codificarUnaVez(str) {
  // Verificación simple: si contiene %, asume que está codificado
  if (/%[0-9A-Fa-f]{2}/.test(str)) {
    return str;  // Ya codificado
  }
  return encodeURIComponent(str);
}

Prueba rápida

const pruebasMulticapa = [
  { input: 'Hello%20World', layers: 1 },
  { input: 'Hello%2520World', layers: 2 },
  { input: '%25252525', layers: 4 },  // %25 codificado 4 veces
];

pruebasMulticapa.forEach(({ input, layers }) => {
  const detectado = contarCapas(input);
  console.assert(detectado === layers, `Falló: esperado ${layers}, obtenido ${detectado}`);
});

Error #4: Confusión de caracteres reservados

El problema

No saber qué caracteres están reservados lleva a decisiones incorrectas de codificación/descodificación.

Errores comunes:

Codificar ? en una cadena de consulta  // ¡Mal! ? es el delimitador de consulta
No codificar & en un valor            // ¡Mal! & separa parámetros
Codificar / en una ruta               // ¡Generalmente mal! / es el separador de ruta

Qué ocurre

// Mal: codificar el delimitador de consulta
const urlMala = `/search${encodeURIComponent('?q=test')}`;
// → /search%3Fq%3Dtest (¡el ? está codificado!)

// Mal: no codificar & en un valor
const nombre = 'Tom & Jerry';
const urlMala = `/search?query=${nombre}`;
// → /search?query=Tom & Jerry
// El navegador interpreta como: query=Tom y un parámetro llamado "Jerry"

// Correcto:
const urlBuena = `/search?query=${encodeURIComponent(nombre)}`;
// → /search?query=Tom%20%26%20Jerry

La causa raíz

  1. Confusión sobre la estructura de URL
  2. Función de codificación incorrecta (encodeURI vs encodeURIComponent)
  3. Construcción manual de URL sin entender caracteres reservados

Caracteres reservados en URLs

CarácterSignificado¿Codificar en valores?
:Separador protocolo/puertoSí (en valores)
/Separador de rutaNo (en rutas), Sí (en valores)
?Inicio de cadena de consultaNo (como delimitador), Sí (en valores)
#Identificador de fragmentoNo (como delimitador), Sí (en valores)
&Separador de parámetrosNo (como separador), Sí (en valores)
=Separador clave-valorNo (como separador), Sí (en valores)
@Separador de info de usuarioSí (usualmente)

La solución

Solución #1: Usar la función de codificación correcta

// Para codificar URLs COMPLETAS
const urlCompleta = 'https://example.com/ruta con espacios/archivo.html';
const codificado = encodeURI(urlCompleta);
// → 'https://example.com/ruta%20con%20espacios/archivo.html'
// Nota: /, :, ? NO están codificados

// Para codificar COMPONENTES de URL (valores de consulta, segmentos de ruta)
const valor = 'hello/world?test=value';
const codificado = encodeURIComponent(valor);
// → 'hello%2Fworld%3Ftest%3Dvalue'
// Nota: TODOS los caracteres especiales están codificados

Solución #2: Construir URLs apropiadamente

// ❌ Forma incorrecta
const busqueda = 'hello & goodbye';
const url = '/search?q=' + busqueda;  // Se rompe con &

// ✅ Forma correcta - codificar el valor
const url = '/search?q=' + encodeURIComponent(busqueda);

// ✅ Mejor - usar API de URL
const url = new URL('/search', window.location.origin);
url.searchParams.set('q', busqueda);  // Codificación automática
console.log(url.href);

Solución #3: Analizar URLs correctamente

// ❌ Incorrecto - análisis manual
const consulta = window.location.search; // ?name=Tom%20%26%20Jerry
const valor = consulta.split('=')[1];     // 'Tom%20%26%20Jerry'
// Si olvidas descodificar, mostrarás la versión codificada

// ✅ Correcto - usar API de URL
const params = new URLSearchParams(window.location.search);
const valor = params.get('name');  // Descodificado automáticamente: 'Tom & Jerry'

Prevención

Usar utilidades de URL:

// Node.js o navegadores modern os
const { URL, URLSearchParams } = require('url');  // Node.js
// O simplemente usa URL y URLSearchParams globales en navegadores

// Construir URLs de forma segura
const url = new URL('https://example.com/search');
url.searchParams.append('query', 'hello & goodbye');
url.searchParams.append('page', '1');
console.log(url.toString());
// → https://example.com/search?query=hello+%26+goodbye&page=1

Prueba rápida

const pruebasCaracteresReservados = [
  { char: '&', desc: 'Ampersand' },
  { char: '=', desc: 'Igual' },
  { char: '?', desc: 'Interrogación' },
  { char: '#', desc: 'Almohadilla' },
  { char: '/', desc: 'Barra' },
];

pruebasCaracteresReservados.forEach(({ char, desc }) => {
  const valor = `antes${char}despues`;
  const codificado = encodeURIComponent(valor);
  const descodificado = decodeURIComponent(codificado);
  
  console.log(`${desc} (${char}):`);
  console.log(`  Original: ${valor}`);
  console.log(`  Codificado:  ${codificado}`);
  console.log(`  Descodificado:  ${descodificado}`);
  console.assert(descodificado === valor, `${desc} falló ida y vuelta`);
});

Error #5: Usar funciones/métodos de descodificación incorrectos

El problema

Diferentes lenguajes y frameworks tienen diferentes funciones de descodificación. Usar la incorrecta produce resultados incorrectos.

Errores comunes

JavaScript:

// ❌ Incorrecto para parámetros de consulta
decodeURI('hello%20world%26test');  
//  → 'hello world%26test' (no descodifica &)

// ✅ Correcto
decodeURIComponent('hello%20world%26test');  
// → 'hello world&test'

Python:

# ❌ Incorrecto - quote() en vez de unquote()
from urllib.parse import quote
result = quote('hello%20world')  
# → 'hello%2520world' (¡doble codificado!)

# ✅ Correcto
from urllib.parse import unquote
result = unquote('hello%20world')  
# → 'hello world'

PHP:

// Los signos más (+) representan espacios en datos de formulario
$codificado = 'hello+world';

// ❌ urldecode() trata + como espacio
$resultado = urldecode($codificado);  
// → 'hello world'

// ✅ Usa rawurldecode() para mantener + como literal
$resultado = rawurldecode($codificado);  
// → 'hello+world'

// O usa urldecode() si + debe ser espacio (datos de formulario)

La solución

Solución #1: Conoce tus funciones

JavaScript:

  • decodeURI() - para URLs enteras (no descodifica &, =, ?, etc.)
  • decodeURIComponent() - para partes de URL (descodifica todo)

Python:

  • urllib.parse.unquote() - descodificación estándar
  • urllib.parse.unquote_plus() - descodifica + como espacio (para datos de formulario)

PHP:

  • urldecode() - descodifica + como espacio
  • rawurldecode() - no descodifica +

Solución #2: Manejar signos más correctamente

// Si trabajas con datos codificados de formulario donde + significa espacio:
function descodificarDatosFormulario(str) {
  return decodeURIComponent(str.replace(/\+/g, ' '));
}

// Uso
descodificarDatosFormulario('hello+world');  // → 'hello world'
decodeURIComponent('hello+world');  // → 'hello+world' (+ no descodificado)

Solución #3: Probar tu función de descodificación

const cadenasprueba = [
  'hello%20world',      // Espacio
  'hello+world',        // Más
  'hello%2Bworld',      // Más codificado
  'test%26value',       // Ampersand
  '%E4%B8%AD%E6%96%87',    // UTF-8
];

cadenasprueba.forEach(str => {
  console.log(`Entrada:  ${str}`);
  console.log(`decodeURI:          ${decodeURI(str)}`);
  console.log(`decodeURIComponent: ${decodeURIComponent(str)}`);
  console.log('---');
});

Prevención

Crear funciones envolventes:

// Estandarizar descodificación en tu aplicación
function descodificarParametroSeguro(str) {
  if (!str) return '';
  
  try {
    // Reemplazar + con espacio para datos de formulario, luego descodificar
    return decodeURIComponent(str.replace(/\+/g, ' '));
  } catch (e) {
    console.error('Error de descodificación:', e);
    return str;  // Devolver original en error
  }
}

// Usar consistentemente
const consultaUsuario = descodificarParametroSeguro(params.get('q'));

Prueba rápida

// Probar todas las funciones de descodificación con la misma entrada
const entradaPrueba = 'hello%20world%26test';

console.log('Probando:', entradaPrueba);
console.log('decodeURI:         ', decodeURI(entradaPrueba));
console.log('decodeURIComponent:', decodeURIComponent(entradaPrueba));
console.log('Esperado:           hello world&test');

Lista de verificación de depuración

Cuando encuentres problemas de descodificación de URL, usa esta lista:

  • ¿Codificación válida? Verifica secuencias porcentuales malformadas (%ZZ, %2)
  • ¿Charset correcto? Verifica UTF-8 en toda la pila
  • ¿Una o múltiples capas? Cuenta cuántas veces está codificado
  • ¿Caracteres reservados? Asegura manejo apropiado de &, =, ?, etc.
  • ¿Función correcta? ¿Usando decodeURIComponent() vs decodeURI()?
  • ¿Signos más? ¿Deben ser espacios o + literal?
  • ¿Manejo de errores? ¿Envuelto en try-catch?
  • ¿Saneado? ¿Validado ysaneado después de descodificar?

Herramientas para depuración

  1. Nuestro descodificador de URL: Herramienta online gratuita con detección de múltiples capas
  2. DevTools del navegador: console.log(decodeURIComponent(str))
  3. Analizador de URL: Visualiza componentes de URL
  4. Visualizadores hexadecimales: Ver valores de bytes reales

Resumen

ErrorSolución rápidaPrevención
#1 Formato incorrectoValidar antes de descodificarUsar funciones de codificación apropiadas
#2 Discrepancia de codificaciónEstandarizar en UTF-8UTF-8 en todas partes
#3 Descodificación incompletaDescodificar hasta estabilizarEvitar doble codificación
#4 Caracteres reservadosUsar encodeURIComponent()Usar API de URL
#5 Función incorrectaConoce tus funcionesCrear envoltorios

Al entender y solucionar estos 5 errores comunes, manejarás la descodificación de URL como un profesional. Recuerda: valida entradas, descodifica con cuidado, y siempre prueba con casos extremos.


¡Evita estos errores instantáneamente con nuestra herramienta gratuita de descodificación de URL que maneja todos los casos extremos automáticamente!