Parser y descodificador de URL
Analiza cualquier URL en sus componentes · protocolo, host, puerto, ruta, parámetros de consulta y fragmento.
Anatomía de una URL: seis componentes, una larga historia
Una URL se analiza en seis partes conceptuales: scheme://userinfo@host:port/path?query#fragment. El scheme indica al cliente qué protocolo usar (https, http, ftp, mailto, file, data) y es la única parte siempre presente. El componente userinfo (username:password@) es raro en uso moderno; los navegadores suelen quitarlo de las URL mostradas porque ha sido un vector de phishing desde los años 90. El host es la ubicación de red, un nombre de dominio registrado, una dirección IP (IPv4 punto-cuarto o IPv6 entre corchetes) o un nombre especial como localhost. El port es el puerto TCP/UDP (80 por defecto para HTTP, 443 para HTTPS, etc.); si se omite, se aplica el por defecto del scheme. El path es la jerarquía separada por barras que identifica el recurso dentro del host. La query string (todo lo que sigue al ?) lleva pares clave-valor separados por &, usados para filtrado, paginación, tracking, envío de formularios. El fragment (todo lo que sigue al #) es la única parte de la URL que nunca se envía al servidor, se procesa enteramente en el lado del cliente por el navegador para hacer scroll a una sección específica o, en single-page apps, para indicar el estado de la ruta.
El formato de la query string en sí tiene una bifurcación: tradicional ?key=value&key2=value2 con valores percent-encoded según RFC 3986, vs la convención más antigua form-encoded application/x-www-form-urlencoded donde + significa un espacio (originalmente para envíos de formularios HTML). La mayoría de parsers manejan ambos, pero la conversión es asimétrica: %20 siempre se decodifica como espacio; + solo se decodifica como espacio dentro de una query string, nunca dentro de un path. Este es uno de los bugs de parsing de URL más comunes en producción.
Una breve historia de la URL
La URL (originalmente «Universal Document Identifier», luego «Universal Resource Locator») fue inventada por Tim Berners-Lee entre su memo «Information Management: A Proposal» de marzo de 1989 en el CERN (el que su jefe Mike Sendall anotó «Vague but exciting») y las primeras páginas web públicamente navegables de agosto de 1991. La primera URL canónica fue http://info.cern.ch/hypertext/WWW/TheProject.html, publicada el 6 de agosto de 1991. Las discusiones de la IETF de 1992 renombraron los UDI a URL para esquivar una pelea de vocabulario. RFC 1738 («Uniform Resource Locators»), de Berners-Lee, Masinter y McCahill, se publicó en diciembre de 1994 como la primera sintaxis formal de URL. RFC 2396 siguió en agosto de 1998, generalizando las URL en el concepto más amplio de URI. La spec canónica actual es RFC 3986 («URI Generic Syntax»), publicada en enero de 2005, editada por Berners-Lee, Roy Fielding y Larry Masinter, un Internet Standard STD 66, el nivel de madurez más alto de la IETF. RFC 3986 es lo que todo parser de URL nominalmente toma como objetivo. En la práctica, los navegadores modernos divergen de RFC 3986 en numerosos casos límite, razón por la que el WHATWG mantiene un URL Living Standard separado en url.spec.whatwg.org describiendo lo que los navegadores realmente hacen; la spec WHATWG aspira explícitamente a hacer obsoletas RFC 3986 y RFC 3987 con el tiempo, y ambas siguen divergiendo en cosas como el manejo de espacios al inicio, los conjuntos de percent-encoding y la normalización Unicode.
Caracteres no reservados, reservados y percent-encoded
RFC 3986 §2.3 define los caracteres no reservados: los únicos caracteres garantizados como seguros en cualquier componente de URI sin percent-encoding: A-Z, a-z, 0-9, guion (-), punto (.), guion bajo (_) y tilde (~). 66 caracteres en total. Todo lo demás es o un carácter reservado con significado estructural en algún componente (gen-delims (:/?#[]@) y sub-delims (!$&'()*+,;=)) o «otro» y debe ser percent-encoded si aparece en una URI. El percent-encoding (RFC 3986 §2.1) toma la secuencia de bytes de un carácter (en UTF-8 a menos que el scheme diga otra cosa) y reemplaza cada byte con %HH donde HH es el valor hex de dos dígitos del byte. Así una é codificada en UTF-8 (bytes 0xC3 0xA9) se convierte en %C3%A9; la palabra rusa привет se convierte en %D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82: dos bytes por carácter, seis triplets %XX y 36 caracteres percent-encoded de URL para seis letras cirílicas.
Los navegadores muestran los paths percent-encoded de dos formas: la mayoría de navegadores modernos (Chrome, Firefox, Safari) decodifican y renderizan los glifos Unicode originales en la barra de direcciones cuando la codificación es UTF-8 válido, pero copian la forma percent-encoded literal cuando el usuario copia la URL. Los navegadores antiguos y muchos logs web solo muestran la forma percent-encoded, razón por la que las «URL Unicode bonitas» pueden ser engañosas: se ven preciosas en la barra de direcciones y feas en cualquier texto donde se compartan. RFC 3987 («Internationalized Resource Identifiers», IRI), publicada en enero de 2005, formalizó las URL Unicode en su forma sin codificar; Punycode (RFC 3492, marzo de 2003) define cómo se codifican los nombres de dominio internacionalizados en ASCII para DNS, etiqueta a etiqueta: la etiqueta de nivel superior china 中国 se vuelve xn--fiqs8s, de modo que example.中国 se resuelve a nivel DNS como example.xn--fiqs8s. La demostración canónica son las URL IRI de Wikipedia: https://ja.wikipedia.org/wiki/東京 funciona en cualquier navegador moderno aunque la petición subyacente codifique el path como /wiki/%E6%9D%B1%E4%BA%AC.
El WHATWG URL Standard, lo que los navegadores realmente hacen
El RFC 3986 de la IETF dice una cosa; los navegadores hacen algo ligeramente distinto. El WHATWG (el organismo de estandarización de los proveedores de navegadores) mantiene un URL Living Standard separado en url.spec.whatwg.org describiendo la máquina de estados algorítmica que los navegadores realmente ejecutan, incluido el manejo de espacios al inicio, caracteres de control, conjuntos de percent-encoding que varían por componente, y normalización Unicode. La spec WHATWG es lo que el constructor URL del navegador (new URL(input)) implementa, y en lo que Node.js, Deno y Bun han convergido para su parsing de URL incorporado. El parser Ada: escrito en C++ por Yagiz Nizipli, Daniel Lemire y otros, se convirtió en el parser conforme a WHATWG que alimenta el parsing de URL de Node.js desde Node.js 18.16.0 (abril de 2023), sustituyendo al antiguo camino url.parse(); es mensurablemente más rápido que cualquier implementación previa y es el estándar de facto para parsing de URL de alto rendimiento en 2026. RFC 3986 y la spec WHATWG todavía no están plenamente conciliadas, y la divergencia histórica aparece aún en código legacy y versiones antiguas de runtime.
La query string, y la API URLSearchParams
La query string es técnicamente solo «todo lo que sigue al ? y precede al #», la spec realmente no define cómo interpretarla. La convención ?key=value&key=value con separadores & es convención, no requisito. En la práctica, dos formatos de query string dominan: application/x-www-form-urlencoded (el formato de envío de formulario HTML por defecto, donde + significa un espacio) y la convención de query URI estándar (donde el espacio siempre es %20). La API URLSearchParams del navegador (parte del WHATWG URL Living Standard) maneja ambos formatos de manera transparente para el parsing y emite la variante form-encoded al stringify. Las claves repetidas son legales: ?tag=red&tag=blue&tag=green es válido, y URLSearchParams.getAll('tag') retorna ['red', 'blue', 'green']. Distintos frameworks web manejan el caso de claves repetidas de forma distinta: Rails y Express agrupan las claves repetidas en arrays, mientras que PHP sobrescribe los valores anteriores con los posteriores salvo que la clave use la convención de corchetes name[]; es una fuente constante de bugs cross-framework en integraciones de API.
Trampas comunes del parsing de URL
- Doble codificación. Codificar una URL ya codificada produce
%2520(el%mismo se codifica como%25). Aparece cuando las URL viajan por múltiples capas (frontend → backend → analytics) y cada capa «amablemente» codifica una vez más. La solución es decodificar hasta abajo antes de recodificar una sola vez. - + vs %20 en query strings.
+significa un espacio dentro de una query string form-encoded pero significa un+literal dentro de un path o fragment. Mezclar las convenciones produce bugs difíciles de depurar donde «John+Doe» se convierte en «John Doe» en la query pero permanece como «John+Doe» en el path. - Sensibilidad a mayúsculas. El host es insensible a mayúsculas (
EXAMPLE.comyexample.comson el mismo host); el path es sensible a mayúsculas en la mayoría de servidores (Linux/Unix) pero insensible en otros (Windows IIS por defecto). Esto significa que la misma URL puede resolver a contenido distinto según el servidor. - IPv6 en URL. Las direcciones IPv6 contienen dos puntos, que entran en conflicto con el separador host:port. La solución es envolver la dirección IPv6 entre corchetes:
http://[2001:db8::1]:8080/path. Muchos parsers de URL históricamente fallaban en esto; los navegadores modernos y el parser WHATWG lo manejan correctamente. - Tokens OAuth en el fragment. El flujo OAuth 2.0 implicit grant devolvía access tokens en el fragment de URL (
#access_token=...) para que no aparecieran en logs del servidor. Las guías modernas de OAuth desaconsejan este flujo a favor de authorization code con PKCE, pero los sistemas legacy todavía emiten tokens en el fragment. - No-identidad de round-trip. Parsear una URL y re-stringificarla no siempre produce la cadena original, el parser normaliza (decodifica caracteres no reservados percent-encoded, pasa el host a minúsculas, ordena los parámetros de query en algunas implementaciones). No asumas que
parse(url).toString() === url.
Casos de uso comunes
- Depuración de peticiones API. Un endpoint REST con una query string larga es difícil de leer; parsearla muestra cada parámetro en su propia línea.
- Inspección de callback OAuth. Las URL de flujo de auth llevan state, code, scope y access tokens codificados que necesitan decodificarse para depurar sin exponerlos a un servidor.
- Trazado de cadenas de redirección. Cuando una URL redirige a través de varias URL intermedias, parsear cada una ayuda a seguir la cadena e identificar dónde se rompe una redirección.
- Auditoría de etiquetas UTM. Las URL de analytics (
?utm_source=...&utm_medium=...&utm_campaign=...) son más fáciles de leer parámetro a parámetro que como un muro de query string. - Auditoría de seguridad. Buscar patrones de inyección SQL o secuencias de path traversal en parámetros de URL; el parsing expone cada valor por separado para revisión.
- Parsing de body form-encoded. El mismo formato usado en query strings de URL se usa en bodies POST
application/x-www-form-urlencoded. - Inspección de deep link. Los deep links de apps móviles y las rutas de webapps codifican estado complejo en el path o la query; el parsing hace visible la estructura.
- Revisión de enlaces de afiliación / compartición. Los enlaces de tracking de campañas de email o programas de afiliación llevan URL de redirección codificadas e IDs de tracking que se benefician de decodificar.
Privacidad: las URL llevan secretos reales
Las URL no se tratan generalmente como secretas, pero a menudo llevan datos que sí lo son. Las URL de callback OAuth incluyen access tokens. Las URL de magic-link login incluyen tokens de autenticación de un solo uso. Los enlaces de restablecimiento de contraseña incluyen reset tokens. Las URL de API internas incluyen nombres de host internos y rutas que revelan infraestructura. Incluso las URL de aplicación ordinarias revelan comportamiento de usuario a través de los parámetros de query, términos de búsqueda, selecciones de filtros, IDs de perfil, identificadores de sesión. La cabecera Referer filtra la URL anterior a cada sitio enlazado, mitigado por la cabecera Referrer-Policy introducida como W3C Candidate Recommendation en 2017 (los valores por defecto siguen variando según el navegador). Las URL acaban en logs de acceso del servidor, en historial del navegador, en marcadores, en logs de CDN, en pipelines de analytics, en previews de enlaces de apps de chat. Un parser de URL del lado del servidor ve cada URL pegada en él; un parser solo en navegador, no. Para URL de API internas, callbacks OAuth con tokens, enlaces de restablecimiento de contraseña, o cualquier URL que no querrías ver copiada en el disco duro de un desconocido, un parser solo navegador es la arquitectura correcta. Verifica en la pestaña Network de DevTools mientras parseas, o pon la página offline (modo avión) tras cargar.
Preguntas frecuentes
¿Qué partes contiene una URL?
Seis partes conceptuales: scheme (https, http, ftp, mailto), userinfo (raro en uso moderno, mayormente quitado por los navegadores como mitigación de phishing), host (dominio o IP), port (por defecto 80 para HTTP, 443 para HTTPS), path (jerarquía separada por barras), query (pares clave-valor tras ?) y fragment (tras #, nunca enviado al servidor). La gramática completa está en RFC 3986 §3 (enero de 2005, STD 66) y el WHATWG URL Living Standard.
¿Cómo decodifico caracteres URL-encoded?
El percent-encoding reemplaza caracteres inseguros con un % seguido del código hex del byte: un espacio es %20, dos puntos es %3A, una barra es %2F, una ampersand es %26, la arroba es %40. Los caracteres multi-byte UTF-8 se codifican byte a byte, así que é se convierte en %C3%A9 (dos bytes). El parser decodifica automáticamente todos los caracteres percent-encoded en la salida mostrada. Las funciones JavaScript estándar son encodeURIComponent() para codificar valores individuales y decodeURIComponent() para decodificar.
¿Qué es un fragment de URL?
El fragment (todo lo que sigue al #) es la única parte de la URL que se procesa enteramente en el cliente, nunca se envía al servidor web en peticiones HTTP. Propósito original: hacer scroll del navegador hasta un elemento ancla con ese ID. Usos modernos incluyen estado de ruta de single-page applications (#/dashboard/profile), tokens de flujo implícito OAuth (hoy desaconsejados a favor de authorization code con PKCE), y navegación por página de PDF (file.pdf#page=5). Como los fragments no llegan al servidor, son un lugar para esconder valores que no deberían aparecer en logs del servidor.
¿Por qué + a veces significa espacio y a veces +?
Existen dos convenciones de codificación. application/x-www-form-urlencoded (el formato de envío de formulario HTML por defecto) codifica los espacios como +; el percent-encoding estándar (según RFC 3986) codifica los espacios como %20. Ambos son válidos en query strings; solo %20 es válido en paths y fragments. URLSearchParams maneja ambos de manera transparente. El bug cross-context surge cuando el código usa encodeURIComponent (que codifica espacio como %20) para parámetros de query que el servidor espera en form-encoded, o viceversa.
¿Maneja URL relativas?
El parser espera una URL completa con scheme. Para un path relativo como /api/users, antepón una URL base (https://example.com/api/users) para parsearla. Algo de parsing de URL relativa (resolución contra una URL base como hace el navegador para atributos href) está en el roadmap, la forma de dos argumentos del constructor URL del WHATWG (new URL(relative, base)) maneja esto y es lo que el código de producción debería usar.
¿Se envían mis URL a algún sitio?
No. El parsing se ejecuta enteramente en tu navegador vía el constructor URL del WHATWG, la URL que pegas nunca sale de tu dispositivo. Verifica en la pestaña Network de DevTools mientras haces clic en Parse, o pon la página offline (modo avión) tras cargar. Seguro para URL de callback OAuth con access tokens, enlaces de restablecimiento de contraseña con tokens de un solo uso, URL de API internas que revelan infraestructura, o cualquier URL que no querrías ver copiada en el disco duro de un desconocido.