har

HAR herramientas

Un archivo HAR es una transcripción JSON de todo lo que tu navegador vio en la red: cómo está estructurado, qué significan sus siete fases de tiempo y qué hay que censurar antes de compartirlo.

2 herramientas

§01 GUÍA DEL TEMA

Un HAR es una transcripción, no una captura de paquetes

Un archivo HAR (HTTP Archive) es un único documento JSON que describe lo que un navegador observó en la red. Todo lo interesante vive en log.entries[]: un elemento por cada par petición/respuesta, con el método, la URL, los encabezados, las cookies, los tamaños, un desglose de tiempos fase por fase y — a veces — los cuerpos. El formato es HAR 1.2, un borrador del W3C que nunca se ratificó, y aun así Chrome, Firefox, Safari, Charles y Fiddler emiten dialectos lo bastante parecidos para que los archivos circulen entre ellos.

El modelo mental que te ahorra horas: el archivo lo escribe la propia pila de red del navegador, a posteriori; contiene el resumen de esa pila, no el cable. Ningún registro de negociación TLS, ninguna trama HTTP/2, ningún paquete DNS, solo las duraciones que la pila decidió informar. Las peticiones que salieron antes de abrir DevTools no están, como tampoco la navegación anterior salvo que Preserve log estuviera activo, y los aciertos de caché o de service worker aparecen como entradas que nunca tocaron la red. Dos capturas del «mismo» error casi nunca coinciden.

Anatomía del archivo

log
├─ version "1.2" · creator{name,version} · browser
├─ pages[]     id, title, startedDateTime, pageTimings{onContentLoad,onLoad}
└─ entries[]   pageref, startedDateTime, time, connection, serverIPAddress
   ├─ request  method, url, httpVersion, headers[], cookies[],
   │           queryString[], postData{mimeType,text,params}
   ├─ response status, statusText, redirectURL, headers[], cookies[],
   │           content{size,compression,mimeType,text,encoding}
   ├─ cache    beforeRequest, afterRequest
   └─ timings  blocked, dns, connect, ssl, send, wait, receive

Dos detalles pillan a la gente. request.headers[] y request.cookies[] son representaciones separadas de las mismas cookies, así que una limpieza que solo recorra los encabezados deja el tarro intacto. Las claves que empiezan por _ (_initiator, _priority, _resourceType, _webSocketMessages) son extensiones propias del fabricante: útiles en Chrome, ausentes en el resto. Y casi cualquier campo numérico puede valer -1no disponible, no cero — incluidos bodySize, pageTimings y cada fase de tiempo.

Las siete fases de tiempo

En milisegundos, en el orden en que ocurren.

FaseMideUn valor alto suele significar-1 cuando
blockedEspera en cola antes de que la petición salga del clienteEl tope de unas 6 por origen de HTTP/1.1; negociación de proxyNunca estuvo en cola
dnsResolución del nombre del hostResolutor frío, cadena larga de CNAMEConexión reutilizada
connectNegociación TCP — incluye sslOrigen lejano, RTT alto, sin keep-aliveConexión reutilizada
sslNegociación TLS (contada dentro de connect)Cadena de certificados larga, sin reanudación de sesiónHTTP sin cifrar, o reutilización
sendEnvío de los bytes de la peticiónUn cuerpo de subida grande
waitTTFB tras el último byte de la peticiónProcesamiento del backend más un RTT
receiveLectura del cuerpo desde la redCarga útil sin comprimir o sobredimensionada

entry.time es el tiempo total transcurrido, en la práctica blocked + dns + connect + send + wait + receive. Como ssl está anidado dentro de connect, sumar las siete columnas cuenta dos veces la negociación: el error aritmético más habitual al analizar un HAR.

Reconstruir la cascada

Ambos ejes se pueden recuperar del JSON: la posición horizontal es entry.startedDateTime menos el log.pages[].startedDateTime al que pertenece; el ancho es entry.time, dividido en las fases de arriba. entry.connection muestra qué peticiones reutilizaron un socket.

Después lee la forma, no los números. Una escalera — cada petición empezando cuando termina la anterior — es una cadena de dependencias, HTML → JS → API → imagen, y ningún ajuste del servidor la arregla; lo que tiene que adelantarse es el descubrimiento. Un bloque denso cuyo segmento blocked crece a medida que bajas por la lista es cola en el cliente, y por eso HTTP/2 aplana a menudo una cascada sin que ninguna respuesta sea más rápida.

Elegir la herramienta adecuada

Pega primero la captura en el Visor HAR, que además está en la selección para SRE. Aplana log.entries[] en Method / Status / Type / Size / Time / URL con un resumen N requests · X KB · Y ms total, y colorea la celda Status en rojo para los 4xx/5xx y en naranja para los 3xx: la forma más rápida de encontrar el caso raro. Solo metadatos, nunca los cuerpos.

El visor da totales por entrada, no el desglose por fases. Para las fases, consulta el archivo en bruto con el Buscador JSONPath: $..timings para todos los desgloses, o $.log.entries[?(@.time>1000)].request.url para solo las lentas. Si la exportación no se analiza (descarga truncada, pegado con saltos de línea), pásala por el Formateador JSON para obtener la posición exacta del error según el analizador.

Para la columna Status, los Códigos HTTP convierten un número suelto en nombre, clase y significado de una línea, filtrables por código o palabra clave: la manera rápida de zanjar 400 frente a 422. Se detienen en el significado, no en el comportamiento de redirección: su fila 301 solo dice que el recurso se movió de forma permanente. Para leer una cadena, añade esto que viene del propio protocolo: 301 y 302 permiten que un cliente reescriba un POST como GET y descarte el cuerpo, mientras que 307 y 308 no. La comparación completa es la matriz de redirecciones de las herramientas HTTP, que cubre también el lado de los encabezados. Sin captura, con solo una URL, las Cabeceras HTTP la solicitan desde el servidor a api.sitekits.dev y devuelven el estado, el número de saltos de redirección y todos los encabezados de respuesta — el cuerpo nunca se recupera y la URL nunca se almacena. Descodifica el token portador de un 401 con el Decodificador JWT para ver si exp ya había pasado; repítelo con el Probador REST API, que envía desde tu navegador directo al destino, así que el CORS se aplica igual que en tu aplicación.

Trampas

Un HAR en bruto es una credencial

Ahí dentro están las cookies de sesión, Authorization: Bearer …, x-api-key, las URL firmadas, los cuerpos de inicio de sesión y todas las respuestas que tu sesión pudiera leer. El Sanitizador HAR se ejecuta por completo en tu navegador — nada se sube — y sustituye los encabezados sensibles, los parámetros que coinciden con token|key|secret|password|passwd|pwd|auth|session|sig|signature y los valores completos de postData.text / content.text por [REDACTED], informando de cuántos ha detectado. Hazlo incluso si tu navegador te ofreció una exportación «saneada»: lo que esas eliminan varía según la versión.

La censura por patrones no es una garantía

Los nombres de encabezado se comparan con una lista fija y los nombres de parámetro con una expresión regular, así que sobrevive todo lo poco convencional: secretos en los segmentos de ruta de una URL (/v1/reset/9f3c…), los arreglos estructurados cookies[], serverIPAddress (la IP real de tu origen), nombres de host internos. Repasa la salida, pega cualquier URL sospechosa en el Parseador de URL para ver la cadena de consulta descodificada, y busca authorization y set-cookie antes de que el archivo salga de tu equipo. El mismo reflejo vale para las herramientas de privacidad y la selección para seguridad.

content.size y bodySize miden cosas distintas

content.size es la longitud del cuerpo descodificado; bodySize son los bytes realmente recibidos, y content.compression registra el ahorro. Las respuestas servidas desde la caché informan bodySize: 0, así que un total en KB calculado a partir de content.size exagera el coste de red de un sitio comprimido.

FAQ
¿Por qué el tiempo total de mi HAR no coincide con el tiempo de carga que medí?
Porque las peticiones se solapan. Sumar entry.time a lo largo de log.entries[] cuenta dos veces cada milisegundo en el que dos peticiones estaban en vuelo, así que la suma suele ser varias veces el tiempo real. Para el tiempo real, lee log.pages[].pageTimings.onLoad, o toma el intervalo entre el startedDateTime más antiguo y el startedDateTime más reciente sumado a su time.
¿Qué significa realmente una fase wait larga en una entrada HAR?
wait es el tiempo hasta el primer byte medido después de enviar el último byte de la petición, así que cubre el propio procesamiento del servidor más una ida y vuelta por la red. Un wait grueso con un receive fino señala al backend: una consulta lenta, una caché fría, un salto entre regiones. La forma contraria, wait fino y receive grueso, significa que la respuesta era simplemente grande o que el enlace era lento.
He censurado los encabezados Cookie de mi HAR, ¿por qué siguen las cookies de sesión en el archivo?
Porque el HAR guarda las cookies dos veces. request.headers[] contiene la línea Cookie en bruto, mientras que request.cookies[] y response.cookies[] contienen los mismos valores ya analizados como objetos nombre/valor, así que cualquier pasada que solo recorra los encabezados deja el tarro intacto, incluido el Sanitizador HAR de este sitio, que censura los valores de encabezado, los parámetros de consulta y de POST y ambos cuerpos, pero no toca los arreglos cookies[]. Busca «cookies» en el archivo antes de adjuntarlo, y aprovecha para revisar serverIPAddress y cualquier secreto alojado en un segmento de ruta de una URL.
¿Por qué algunas entradas HAR muestran dns -1 y connect -1?
Esas fases no ocurrieron en esa petición. En HAR 1.2, -1 significa que el valor no es aplicable o no está disponible, que es lo que obtienes cuando la petición viajó por una conexión keep-alive existente y por tanto no necesitó resolución DNS, ni negociación TCP, ni negociación TLS. Tratar -1 como cero es inofensivo; promediarlo como si fuera una medición real no lo es.
¿Por qué faltan los cuerpos de petición y respuesta en mi HAR?
postData.text y content.text son campos opcionales, y DevTools omite habitualmente las cargas útiles grandes o binarias para que la exportación siga siendo manejable. Cuando hay un cuerpo, puede estar en Base64 en lugar de texto plano, algo que indica content.encoding con valor base64. Un cuerpo ausente significa que el exportador no lo registró, no que la respuesta estuviera vacía.