Arquitectura de URI según RFC 3986: Caracteres Reservados vs. No Reservados
La especificación del Identificador Uniforme de Recursos (URI, RFC 3986) establece una sintaxis inequívoca para identificar recursos a través de protocolos de red. Dado que los URIs deben transitar a través de sistemas de transporte heterogéneos —incluyendo routers legados, pasarelas de correo electrónico y servidores proxy HTTP—, solo un subconjunto restringido del conjunto de caracteres US-ASCII está autorizado para emplearse directamente dentro de una cadena de URI.
Los caracteres se dividen estrictamente en dos clases principales:
| Clasificación | Conjunto de Caracteres | Sección RFC 3986 | Función Sintáctica |
|---|---|---|---|
| No Reservados (Unreserved) | A-Z a-z 0-9 - _ . ~ | Sección 2.3 | Caracteres que nunca desempeñan una función de delimitador sintáctico y jamás deben codificarse. |
| Delimitadores Genéricos (gen-delims) | : / ? # [ ] @ | Sección 2.2 | Delimitan los componentes estructurales principales del URI (esquema, autoridad, ruta, consulta, fragmento). |
| Subdelimitadores (sub-delims) | ! $ & ' ( ) * + , ; = | Sección 2.2 | Delimitan subcomponentes dentro de componentes mayores, tales como pares clave-valor en query strings. |
Un carácter reservado solo está permitido en forma literal cuando cumple su rol como delimitador entre componentes sintácticos. Cuando un carácter reservado forma parte de los datos dentro de un componente (como un signo et (&) dentro del valor de un parámetro de consulta), debe codificarse obligatoriamente en porcentaje para evitar que los analizadores lo interpreten erróneamente como un delimitador.
Mecanismo de Codificación en Porcentaje y Serialización Multibyte UTF-8
La codificación en porcentaje representa octetos arbitrarios mediante una secuencia de tres caracteres: un signo de porcentaje (%) seguido de dos dígitos hexadecimales que denotan el valor numérico del byte. Al procesar caracteres fuera del rango US-ASCII de 7 bits (puntos de código superiores a U+007F), la sección 2.5 de la norma RFC 3986 estipula un procedimiento de dos fases:
- Transformación a Bytes UTF-8: El carácter Unicode se transforma en su secuencia canónica de octetos UTF-8 (de 1 a 4 bytes).
- Formato en Tripletes de Octetos: Cada byte individual resultante se formatea como
%HHempleando dígitos hexadecimales en mayúsculas.
| Carácter | Punto de Código | Octetos UTF-8 | Resultado Codificado | Clasificación de la Codificación |
|---|---|---|---|---|
| Espacio | U+0020 | 0x20 | %20 | Espacio en blanco / control ASCII no permitido |
| & | U+0026 | 0x26 | %26 | Subdelimitador reservado ASCII |
| é | U+00E9 | 0xC3 0xA9 | %C3%A9 | Unicode de 2 bytes (Latin-1 Supplement) |
| 文 | U+6587 | 0xE6 0x96 0x87 | %E6%96%87 | Unicode de 3 bytes (Ideogramas CJK unificados) |
| 🚀 | U+1F680 | 0xF0 0x9F 0x9A 0x80 | %F0%9F%9A%80 | Unicode de 4 bytes (Plano suplementario multilingüe) |
Comparativa de APIs Estándar: encodeURI() vs. encodeURIComponent()
El entorno de ejecución de JavaScript ofrece dos funciones integradas con ámbitos de aplicación fundamentalmente distintos. Elegir la función inapropiada es una de las causas más frecuentes de corrupción de rutas y parámetros en aplicaciones web:
| Método Estándar | Ámbito de Aplicación Previsto | Caracteres / Delimitadores Preservados | Riesgo Principal en Caso de Uso Incorrecto |
|---|---|---|---|
| encodeURI() | Una cadena de URI completa y debidamente estructurada. | : / ? # [ ] @ ! $ & ' ( ) * + , ; = | Deja delimitadores sin escapar en valores de parámetros, corrompiendo la separación de claves. |
| encodeURIComponent() | Un componente aislado (segmento de ruta, clave o valor de consulta). | A-Z a-z 0-9 - _ . ! ~ * ' ( ) | Escapa las barras del protocolo y del host (https%3A%2F%2F), inutilizando el enrutamiento. |
La Discrepancia del Espacio: %20 Frente al Signo Más (+)
Un motivo habitual de confusión en ingeniería es si los espacios en blanco deben codificarse como %20 o como +:
- Estándar RFC 3986: Exige
%20en todos los componentes del URI. El signo más está catalogado como subdelimitador reservado y representa un carácter más literal a menos que se escape como%2B. - Especificación de Formularios HTML (application/x-www-form-urlencoded): Estándar histórico donde los campos de formularios enviados mediante HTTP GET o POST sustituyen los espacios por
+. - Firmas Criptográficas en APIs: Especificaciones modernas —incluidas OAuth 1.0a, AWS Signature Version 4 y OpenAPI— imponen de forma estricta la codificación en porcentaje RFC 3986. Utilizar
+en lugar de%20genera discrepancias en el cálculo de hashes y fallos de autenticación.
Errores Habituales en la Codificación de URLs y Técnicas de Depuración
Los incidentes en entornos de producción suelen originarse por interpretaciones incorrectas de los límites de los URIs y decodificaciones multicapa:
1. La Cascada de Doble Codificación
Cuando una cadena que ya contiene entidades codificadas en porcentaje vuelve a procesarse por un codificador, cada signo de porcentaje (%) se convierte en %25. Por ejemplo, un espacio previamente codificado (%20) se transforma en %2520. Cuando el servidor de destino efectúa una única pasada de decodificación, obtiene la cadena literal %20 en vez de un espacio.
2. Fuga de Parámetros en URLs Anidadas
Al enviar una URL de redirección como parámetro de consulta (p. ej., ?redirect=https://example.com/callback?a=1&b=2), omitir la codificación completa con encodeURIComponent() ocasiona que el analizador externo interprete &b=2 como un parámetro de la URL principal en lugar de la carga útil anidada.
3. Secuencias Multibyte Truncadas
Ejecutar decodeURIComponent() sobre una secuencia incompleta —como el primer byte de un carácter chino (%E6) desprovisto de sus dos bytes de continuación— genera inmediatamente una excepción URIError: URI malformed. Las aplicaciones robustas envuelven las operaciones de decodificación en bloques defensivos try/catch.
Guía de Implementación en Bibliotecas Estándar por Lenguaje
Las implementaciones nativas en los principales entornos backend gestionan la codificación RFC 3986 mediante utilidades estándar:
// Construcción segura de URLs con URLSearchParams
const params = new URLSearchParams();
params.set('search', 'hola mundo');
params.set('filter', 'categoria&etiqueta');
const url = 'https://api.example.com/items?' + params.toString();
// Salida: https://api.example.com/items?search=hola+mundo&filter=categoria%26etiqueta
// Codificación estricta RFC 3986 (garantizando %20 para espacios)
const strictEncode = (str) => encodeURIComponent(str).replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase());import urllib.parse
# Codificación en porcentaje según RFC 3986 (%20 para espacios)
encoded_component = urllib.parse.quote("hola mundo & datos", safe="")
# Salida: 'hola%20mundo%20%26%20datos'
# Decodificación de cadena codificada
decoded = urllib.parse.unquote(encoded_component)
# Codificación de formulario (application/x-www-form-urlencoded con + para espacios)
form_encoded = urllib.parse.quote_plus("hola mundo & datos")
# Salida: 'hola+mundo+%26+datos'package main
import (
"fmt"
"net/url"
)
func main() {
// QueryEscape codifica para parámetros de consulta (+ para espacios)
query := url.QueryEscape("hola mundo & clave")
// PathEscape codifica para segmentos de ruta (%20 para espacios)
path := url.PathEscape("usuario carpeta/hola mundo")
// Decodificación
unescaped, _ := url.QueryUnescape(query)
fmt.Println(query, path, unescaped)
}import java.net.URLEncoder;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
public class UrlCodec {
public static void main(String[] args) throws Exception {
String raw = "hola mundo & usuario";
// Codifica con + para espacios; sustituir por %20 para RFC 3986 estricto
String encoded = URLEncoder.encode(raw, StandardCharsets.UTF_8.toString())
.replace("+", "%20");
String decoded = URLDecoder.decode(encoded, StandardCharsets.UTF_8.toString());
}
}<?php
$input = "hola mundo & datos";
// Codificación conforme a RFC 3986 (%20 para espacios)
$rfc3986 = rawurlencode($input);
// Salida: hola%20mundo%20%26%20datos
// Codificación tradicional de formulario (+ para espacios)
$formEncoded = urlencode($input);
// Salida: hola+mundo+%26+datos
// Decodificación
$decoded = rawurldecode($rfc3986);
?>Frequently Asked Questions
¿Cuál es la diferencia entre el modo Componente (encodeURIComponent) y el modo URL completa (encodeURI)?
El modo Componente utiliza encodeURIComponent(), que escapa todos los delimitadores reservados de la URL como :, /, ?, #, & y =. Está diseñado para codificar valores de parámetros en cadenas de consulta o fragmentos de ruta, evitando que colisionen con la sintaxis de la URL externa. El modo URL completa utiliza encodeURI(), que preserva los caracteres estructurales de la URL (https://, /, ?, &) y codifica únicamente espacios y caracteres no ASCII, siendo óptimo para sanitizar una dirección web completa sin alterar su capacidad de navegación.
¿Por qué un espacio se codifica como %20 en lugar de un signo más (+)?
La especificación RFC 3986 define el estándar genérico para URIs, donde los espacios deben codificarse en porcentaje obligatoriamente como %20. El signo más (+) procede de la especificación histórica application/x-www-form-urlencoded utilizada en el envío de formularios HTML. En APIs REST modernas, segmentos de ruta y algoritmos de firma criptográfica (como AWS Signature Version 4 y OAuth 1.0a), se exige estrictamente %20 porque el carácter + representa un signo más literal.
¿Cómo se codifica una URL de redirección o callback anidada dentro de un parámetro de consulta?
Al incrustar una URL completa como valor de un parámetro de consulta (por ejemplo, ?redirect=https://example.com/auth?step=2&client=web), seleccione el modo Componente (encodeURIComponent). Esto transforma los caracteres internos ?, & y / en %3F, %26 y %2F, garantizando que el analizador del servidor receptor no interprete los parámetros anidados como campos de la petición principal.
¿Cómo procesa esta herramienta caracteres Unicode no ASCII y emojis?
Según la sección 2.5 de la norma RFC 3986, los caracteres no ASCII se transforman primero en sus secuencias de bytes equivalentes en UTF-8 (de 1 a 4 bytes). A continuación, cada byte individual se formatea como un triplete hexadecimal en mayúsculas (%HH). Por ejemplo, la vocal acentuada é genera dos bytes (%C3%A9), un carácter como 文 genera tres bytes (%E6%96%87), y un emoji como 🚀 produce cuatro bytes (%F0%9F%9A%80).
¿Qué provoca el error de secuencia no válida durante la decodificación?
Esta excepción (URIError en JavaScript) ocurre cuando la entrada contiene una secuencia de porcentaje mal formada que infringe las reglas de decodificación UTF-8. Entre las causas más habituales se encuentran un símbolo % aislado sin dos dígitos hexadecimales válidos posteriores, una secuencia UTF-8 multibyte truncada a la que le faltan bytes de continuación, o pares subrogados UTF-16 huérfanos.
¿Qué es la doble codificación de URL (p. ej., %2520, %252F) y cómo se soluciona?
La doble codificación ocurre cuando una cadena previamente codificada se procesa de nuevo por un codificador, convirtiendo el propio símbolo % en %25 (por ejemplo, %20 pasa a ser %2520). Para resolverlo en esta herramienta, pegue el texto doblemente codificado en el panel de texto codificado para revertirlo a su forma con codificación simple, o realice una segunda pasada de decodificación para obtener el texto original sin procesar.
¿Qué caracteres NO codifica esta herramienta en el modo Componente?
El modo Componente utiliza la función estándar encodeURIComponent(), que mantiene sin codificar caracteres alfanuméricos (A–Z, a–z, 0–9) y los símbolos -, _, ., !, ~, *, ', ( y ). Cabe señalar que, aunque ECMAScript conserva !, ~, *, ', ( y ) por compatibilidad histórica con la RFC 2396, la norma RFC 3986 clasifica !, *, ', ( y ) como subdelimitadores reservados. Si el sistema de destino exige cumplimiento estricto de la RFC 3986 (como firmas OAuth 1.0a o AWS SigV4), dichos 5 caracteres deben escaparse manualmente como %21, %2A, %27, %28 y %29.
