El motivo por el que escribimos esto: somos Geonode y vendemos proxies a personas que recopilan datos, y gran parte de esos datos llegan en formato XML: mapas de sitio, fuentes RSS y Atom, respuestas SOAP, catálogos de productos. La verdad es que un error de análisis casi nunca se debe a un problema de red. Si tu analizador XML te da un error, muestra los primeros 200 caracteres de lo que has recibido antes de modificar ningún código. Nueve de cada diez veces se trata de una página de error HTML, y el analizador está indicando correctamente que no se te ha enviado XML.
La trampa del error: no lanza una excepción
El comportamiento que pilla a todo el mundo la primera vez.
Si se introduce un XML malformado en DOMParser, no se genera ninguna excepción. MDN lo deja claro: «el objeto XMLDocument devuelto contendrá un nodo <parsererror> que describe el error de análisis», y el error «también puede notificarse en la consola de JavaScript del navegador».
Por lo tanto, debes comprobar:
const doc = parser.parseFromString(xmlString, "application/xml");
const errorNode = doc.querySelector("parsererror");
if (errorNode) {
throw new Error(`XML parse failed: ${errorNode.textContent}`);
}
Sin esa comprobación, un documento con formato incorrecto genera un objeto Document que contiene un mensaje de error donde deberían estar tus datos. Tu posterior querySelectorAll no devuelve nada, y el síntoma parece un problema de selector en lugar de un fallo de análisis.
Envuélvelo una vez:
function parseXml(text) {
const doc = new DOMParser().parseFromString(text, "application/xml");
const err = doc.querySelector("parsererror");
if (err) {
throw new Error(
`XML parse failed: ${err.textContent.trim()}. ` +
`First 200 chars: ${text.slice(0, 200)}`
);
}
return doc;
}
El text.slice(0, 200) es la parte que ahorra tiempo. Un error de análisis te indica que la entrada no era válida; los primeros 200 caracteres te indican que se trataba de una página de error HTML.
En Node: cómo elegir una biblioteca
Node no cuenta con un analizador XML integrado, por lo que se trata de una decisión que depende de las bibliotecas. Las cifras de descargas semanales ofrecen una idea aproximada de su adopción; estas proceden del registro de npm y se consultaron en septiembre de 2026.
| Biblioteca | Descargas semanales | Estilo |
|---|---|---|
sax |
| ~88,9 millones | Streaming, basado en eventos |
| fast-xml-parser
| ~85,0 millones | De XML a objetos JavaScript simples |
| @xmldom/xmldom
| ~48,7 millones | Implementación DOM para Node |
| xml2js
| ~44,5 M | Conversión de XML a objetos, API de callback y promise |
| xpath
| ~12,2 M | Consultas XPath, se combina con xmldom |
**fast-xml-parser
** es la opción predeterminada más práctica para la mayoría de los trabajos. Convierte XML en objetos JavaScript normales, lo que significa que se navega mediante notación de puntos en lugar de métodos DOM:
import { XMLParser } from "fast-xml-parser";
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@" });
const obj = parser.parse(xmlString);
console.log(obj.rss.channel.item[0].title);
Hay que tener en cuenta una desventaja: la conversión a objetos conlleva una pérdida de información en un aspecto concreto. Un elemento que aparece una sola vez se convierte en un objeto; el mismo elemento que aparece dos veces se convierte en una matriz. Así, channel.item
es una matriz para un feed con tres elementos y un objeto para uno con uno solo; y el código que da por hecho que se trata de una matriz fallará en el caso de un único elemento. La mayoría de las bibliotecas ofrecen una opción para generar siempre matrices con los elementos con nombre, y merece la pena activarla para todo aquello sobre lo que vayas a iterar antes de que te dé problemas.
**@xmldom/xmldom
** te proporciona un DOM real en Node, lo cual es importante si quieres que el mismo código funcione en ambos entornos o si necesitas XPath. Combínalo con el paquete xpath
:
import { DOMParser } from "@xmldom/xmldom";
import xpath from "xpath";
const doc = new DOMParser().parseFromString(xmlString, "text/xml");
const titles = xpath.select("//item/title/text()", doc);
**sax
** es un analizador de flujo continuo que emite eventos a medida que lee. Es la solución para documentos demasiado grandes para caber en memoria —un índice de mapa del sitio de varios gigabytes, una exportación masiva de un catálogo— en los que un enfoque basado en DOM simplemente no es adecuado.
**xml2js
** cuenta con una larga trayectoria y es muy utilizado. Su API está un poco desfasada, pero es totalmente funcional, y existe una gran cantidad de código que la utiliza.
Espacios de nombres: lo que hace que los documentos reales dejen de funcionar
La razón más habitual por la que un selector que funcionaba deja de encontrar nada de repente.
Muchos formatos XML reales declaran espacios de nombres:
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://example.com/</loc></url>
</urlset>
xmlns
coloca todos los elementos en un espacio de nombres predeterminado. En un analizador que tiene en cuenta los espacios de nombres, <loc>
no es simplemente loc
, sino loc
en el espacio de nombres «sitemaps», y un simple getElementsByTagName("loc")
puede que no encuentre nada.
Tres formas de gestionarlo, en orden creciente de corrección.
Utiliza los métodos que tienen en cuenta los espacios de nombres:
const NS = "http://www.sitemaps.org/schemas/sitemap/0.9";
const locs = doc.getElementsByTagNameNS(NS, "loc");
Utiliza un espacio de nombres comodín cuando no te importe cuál sea:
const locs = doc.getElementsByTagNameNS("*", "loc");
Configura tu biblioteca para que ignore los espacios de nombres. La mayoría de las bibliotecas de mapeo de objetos tienen una opción para eliminar los prefijos de los espacios de nombres, lo que genera claves simples del tipo loc
. Es cómodo, pero fusionará silenciosamente dos elementos genuinamente diferentes que, por casualidad, compartan un nombre local —lo cual es aceptable para un mapa del sitio, pero peligroso para un documento que mezcle vocabularios—.
Ten en cuenta que querySelector
se comporta de forma diferente a getElementsByTagNameNS
en este caso: los selectores CSS tienen su propia sintaxis de espacio de nombres, que resulta poco práctica y rara vez se utiliza, por lo que, para XML con espacios de nombres, los métodos NS
o XPath son más fiables.
XPath en JavaScript
Disponible en los navegadores a través de document.evaluate, y en Node mediante el paquete xpath con xmldom.
const result = doc.evaluate(
"//item/title/text()",
doc,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
for (let i = 0; i < result.snapshotLength; i++) {
console.log(result.snapshotItem(i).nodeValue);
}
La API es tan prolija que la mayoría de la gente la integra una vez y se olvida de ella. Lo que hace que merezca la pena el esfuerzo es que XPath expresa cosas que los selectores CSS no pueden: coincidencia con contenido de texto, navegación hacia antepasados y lógica posicional relativa a elementos hermanos.
Hay dos limitaciones. Los navegadores implementan XPath 1.0, por lo que no se admiten matches(), lower-case() ni ends-with(). Además, los espacios de nombres requieren una función de resolución —el tercer argumento— que asigne los prefijos a los URI de los espacios de nombres. Pasar null solo funciona para documentos sin espacios de nombres, lo que excluye la mayoría de los feeds reales.
Para documentos con espacios de nombres en un navegador:
const resolver = prefix => ({ sm: "http://www.sitemaps.org/schemas/sitemap/0.9" }[prefix] || null);
const result = doc.evaluate("//sm:loc/text()", doc, resolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
Ten en cuenta que debes inventar el prefijo —en este caso, sm— independientemente de cuál utilice el documento, ya que XPath 1.0 no contempla el concepto de espacio de nombres predeterminado.
Conversión de XML a JSON, y lo que se pierde
Lo que la gente suele querer, y conviene tener claro, es que la conversión no es sin pérdidas.
XML y JSON tienen modelos de datos diferentes. XML tiene atributos, elementos, nodos de texto, comentarios, instrucciones de procesamiento, espacios de nombres y orden. JSON tiene objetos, matrices, cadenas, números, valores booleanos y null. Hay cuatro elementos que no se conservan correctamente en la conversión.
Atributos frente a elementos hijos. <item id="1"><name>x</name></item> tiene un atributo y un elemento hijo, y JSON no distingue entre ambos. Las bibliotecas gestionan esto añadiendo un prefijo a las claves de los atributos —normalmente @ o $—, que tú configuras y luego debes recordar:
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@" });
// { item: { "@id": "1", name: "x" } }
Los elementos repetidos se convierten en matrices de forma inconsistente. Ya se ha tratado anteriormente, pero merece la pena repetirlo porque es el error más común en este ámbito: una aparición da lugar a un objeto, dos dan lugar a una matriz. Activa la opción «siempre matriz» de la biblioteca para cada elemento que pretendas iterar.
El contenido mixto no tiene una representación clara. <p>Hello <b>world</b>!</p> entremezcla texto y elementos. Al convertirse en un objeto, los fragmentos de texto y sus posiciones relativas al elemento hijo son difíciles de representar, y la mayoría de las bibliotecas o bien concatenan el texto o bien omiten partes del mismo. Si tu XML contiene marcado de prosa, la conversión a objeto no es el enfoque adecuado: mantén el DOM.
El orden no está garantizado. Las claves de los objetos JSON no tienen un orden definido en el modelo de datos, por lo que un documento en el que la secuencia de elementos tenga significado pierde esa información. Las matrices conservan el orden; los elementos hermanos con nombres diferentes, no.
Y todo se convierte en una cadena a menos que se indique lo contrario. El XML no tiene tipos, por lo que <price>42.50</price> es texto. La mayoría de las bibliotecas ofrecen coerción numérica, lo cual es práctico y convertirá sin problemas un código de producto con ceros iniciales en un número y una cadena de versión en un número flotante. Para cualquier cosa que sea un identificador en lugar de una cantidad, desactiva la coerción.
Consejo práctico: la conversión a objetos es adecuada para el XML con estructura de datos —feeds, catálogos, configuraciones, respuestas de API— donde los elementos son registros y campos. Mantén un DOM para el XML con estructura de documento, donde el marcado está incrustado en el texto y la estructura tiene significado.
Seguridad: XXE e inyección
Dos riesgos distintos, ambos reales.
Procesamiento de entidades externas en XML. El XML puede declarar entidades que hacen referencia a recursos externos, incluidos archivos locales y URL de red. Se puede manipular un analizador que las resuelva para que lea archivos del servidor o realice solicitudes en nombre de un atacante. Se trata de un tipo de vulnerabilidad clásico y que sigue siendo habitual.
La medida de mitigación consiste en desactivar el procesamiento de entidades externas y DTD en cualquier analizador que se utilice. El navegador DOMParser no resuelve entidades externas, por lo que, por defecto, el navegador es seguro. Las bibliotecas de Node varían, por lo que conviene comprobarlo en lugar de darlo por sentado: si estás analizando XML procedente de una fuente no fiable en Node, confirma cómo gestiona las entidades tu analizador antes de lanzarlo.
Inyección al reinsertar en el DOM. MDN advierte de que parseFromString «es un “sumidero de inyección” y un vector potencial para ataques XSS si la entrada procede de un atacante». El matiz es importante: con text/html, «los elementos<script> se marcan como no ejecutables y no se invocan los controladores de eventos», pero los scripts «se ejecutarán si el documento analizado se inyecta posteriormente en el DOM visible».
Por lo tanto, el análisis es seguro; la inserción del resultado, no. Las recomendaciones de MDN son pasar objetos TrustedHTML en lugar de cadenas, aplicar tipos de confianza mediante la directiva CSP require-trusted-types-for y depurar el contenido con una biblioteca como DOMPurify a través de un TrustedTypePolicy.
La regla es sencilla: nunca insertes código de marcado analizado y no fiable en el DOM activo sin depurarlo primero, y da preferencia a textContent sobre innerHTML cuando solo necesites el texto.
Patrones prácticos
Análisis de una fuente RSS o Atom:
const doc = parseXml(await res.text());
const items = [...doc.getElementsByTagNameNS("*", "item")].map(item => ({
title: item.getElementsByTagNameNS("*", "title")[0]?.textContent?.trim(),
link: item.getElementsByTagNameNS("*", "link")[0]?.textContent?.trim(),
date: item.getElementsByTagNameNS("*", "pubDate")[0]?.textContent?.trim(),
}));
El espacio de nombres comodín admite tanto RSS como Atom sin necesidad de ramificaciones, y el encadenamiento opcional gestiona las fuentes a las que les faltan campos —que son la mayoría—.
Análisis de un mapa del sitio, incluidos los archivos de índice:
const doc = parseXml(xml);
const isIndex = doc.documentElement.localName === "sitemapindex";
const locs = [...doc.getElementsByTagNameNS("*", "loc")].map(n => n.textContent.trim());
// if isIndex, these are sitemap URLs to fetch; otherwise they are page URLs
Comprobar el atributo ``localName`
del elementodocument es la forma más fiable de distinguir entre ambos, ya que ambos contienen elementos ``<loc>
` y solo difieren en el contenedor.
Gestión de un documento de gran tamaño: utiliza un analizador de flujo en lugar de construir un DOM:
import sax from "sax";
const stream = sax.createStream(true, { trim: true });
let current = null;
stream.on("opentag", node => { if (node.name === "loc") current = ""; });
stream.on("text", t => { if (current !== null) current += t; });
stream.on("closetag", name => { if (name === "loc") { emit(current); current = null; } });
Memoria constante independientemente del tamaño del documento, a cambio de escribir una pequeña máquina de estados.
Preguntas frecuentes
¿Cómo puedo analizar un XML en JavaScript?
En un navegador, utiliza el analizador integrado «DOMParser»: new DOMParser().parseFromString(xml, "application/xml") devuelve un «Document» que puedes consultar con los métodos DOM. En Node no hay ningún analizador integrado, por lo que debes instalar uno: fast-xml-parser para la conversión a objetos, @xmldom/xmldom para un DOM real.
¿Por qué DOMParser no lanza una excepción ante un XML no válido?
Es por diseño. En lugar de una excepción, el documento devuelto contiene un nodo <parsererror> que describe el error. Hay que comprobarlo explícitamente con doc.querySelector("parsererror"); de lo contrario, un documento mal formado producirá resultados de consulta vacíos sin avisar.
¿Tiene Node.js un analizador XML integrado?
No. A diferencia de JSON, el XML requiere una dependencia. Las opciones más utilizadas son fast-xml-parser y xml2js para la conversión a objetos, @xmldom/xmldom para una implementación DOM, y sax para el procesamiento en streaming de documentos muy grandes.
¿Por qué mi selector XML no encuentra nada?
Normalmente, por los espacios de nombres. Un documento que declare xmlns coloca todos los elementos en ese espacio de nombres, y un simple getElementsByTagName puede que no coincida. Utiliza getElementsByTagNameNS con el URI del espacio de nombres, o "*" como comodín, o configura tu biblioteca para que ignore los espacios de nombres.
¿Cómo utilizo XPath con XML en JavaScript?
En los navegadores, document.evaluate con un tipo XPathResult —lo suficientemente detallado como para envolverlo una vez—. En Node, el paquete xpath junto con @xmldom/xmldom. Ten en cuenta que los navegadores solo implementan XPath 1.0, y los documentos con espacio de nombres necesitan una función de resolución que asigne prefijos a los URI.
¿Supone un riesgo de seguridad analizar XML en JavaScript?
Hay dos riesgos. El procesamiento de entidades externas XML puede hacer que un analizador lea archivos locales o envíe solicitudes; el navegador DOMParser no resuelve entidades externas, pero las bibliotecas de Node varían y deben comprobarse. Además, insertar marcado analizado no fiable en el DOM activo puede ejecutar scripts, por lo que hay que depurarlo antes de insertarlo.
¿Cómo analizo un archivo XML muy grande?
Utiliza un analizador de flujo continuo como sax, que emite eventos a medida que lee en lugar de construir un documento en memoria. Esto permite gestionar archivos de cualquier tamaño con un consumo de memoria constante, a cambio de tener que escribir una pequeña máquina de estados para realizar un seguimiento de la posición en la que te encuentras.
¿Por qué mi XML analizado a veces devuelve un array y otras veces un objeto?
Porque las bibliotecas de mapeo de objetos solo generan un array cuando un elemento se repite. Un feed con tres elementos te da un array; el mismo feed con un solo elemento te da un objeto. La mayoría de las bibliotecas tienen una opción para generar siempre arrays para los elementos con nombre: actívala para todo aquello que iteres.
Conclusión
El entorno es lo que determina en gran medida todo esto. En un navegador, tienes DOMParser y no hay dependencias; en Node, eliges una biblioteca, y esa elección determina cómo escribes todo lo que viene después.
Hay dos comportamientos que explican la mayor parte del tiempo que se pierde. DOMParser informa de los fallos con un nodo parsererror en lugar de una excepción, por lo que un documento mal formado arroja resultados vacíos que parecen un problema de selector: comprueba ese nodo y registra los primeros 200 caracteres de la entrada, ya que suele tratarse de una página de error HTML. Además, los espacios de nombres impiden silenciosamente las búsquedas simples de nombres de etiquetas precisamente en los documentos que más te interesa analizar: los mapas de sitio, los feeds y las respuestas SOAP los declaran todos.
Más allá de eso, adapta la herramienta al tamaño. Mapeo de objetos para documentos comunes, un DOM real cuando necesites XPath o código compartido entre el navegador y el servidor, y un analizador de flujo continuo cuando el archivo sea demasiado grande para almacenarlo. Y si la fuente no es de confianza, comprueba cómo gestiona tu analizador Node las entidades externas antes de lanzarlo; esa ha sido una clase de vulnerabilidad durante dos décadas y sigue siéndolo.
