El reporte, cuando finalmente llega, es que la página dejó de actualizarse. No apareció ningún error. Los números simplemente dejaron de moverse, y una recarga lo arregló.

Por eso es que rara vez oyes hablar de ello. La persona que lo experimenta tiene una solución que funciona en sus manos en cuestión de segundos, así que la mayoría nunca te lo dicen. Los que sí te lo dicen no tienen nada que describir excepto una ausencia.

Un socket cerrado no es un error

Lo primero que tienes que desaprender es que esto llegará como una solicitud fallida. No lo hará.

Un WebSocket sí dispara un evento error, y MDN describe su tipo en tres palabras: “Un Event genérico”. No hay ninguna interfaz personalizada debajo, lo que significa que no hay code, ningún reason y ningún estado en el objeto que se te entrega. Nada lanza una excepción, así que nada llega a un try/catch. La conexión termina, y la única cosa que lleva información es el evento close que sigue.

Así que el código de cierre no es un detalle. Es el mensaje completo.

Los códigos que significan algo

Cuatro merecen la pena conocer de memoria, según las propias palabras de MDN.

  • 1000, cierre normal: “La conexión completó exitosamente el propósito para el que fue creada”. Nada que investigar.
  • 1001, se va: “El endpoint se va, ya sea por un fallo del servidor o porque el navegador está navegando lejos de la página que abrió la conexión”.
  • 1011, error interno: “El servidor está terminando la conexión porque encontró una condición inesperada que le impidió cumplir la solicitud”.
  • 1006, cierre anormal, que es el que realmente verás y del que trata el resto de este artículo.

Lee 1001 dos veces, porque cubre dos eventos no relacionados con un mismo número. Un servidor cerrándose y un usuario haciendo clic en un enlace ambos producen 1001. Si estás contando 1001s como incidentes, estás contando a personas que se van de tu sitio como incidentes, y el número seguirá tu tráfico en lugar de tu confiabilidad.

1011 es lo opuesto y es el que vale la pena alertar. Es el servidor admitiendo el fallo con sus propias palabras.

El código que ves más a menudo no pudo haber venido de nadie

Aquí está la parte que casi nunca está escrita, y cambia cómo investigas.

RFC 6455 define 1006 así:

1006 es un valor reservado y NO DEBE ser establecido como código de estado en un marco de control Close por un endpoint. Está designado para uso en aplicaciones que esperan un código de estado para indicar que la conexión fue cerrada anormalmente, por ejemplo, sin enviar o recibir un marco Close.

1005 lleva la misma prohibición: “1005 es un valor reservado y NO DEBE ser establecido como código de estado en un marco de control Close por un endpoint”.

Así que ninguno de los dos puede ser enviado. No por tu servidor, no por el navegador, no por nada en el medio. Están prohibidos en la red. Cuando tu manejador lee 1006, está leyendo un número que tu propio navegador inventó localmente para describir el hecho de que la conexión terminó y ningún marco close llegó nunca.

Esa es una afirmación más fuerte de lo que parece. Significa que no hay ningún evento correspondiente en otro lugar al que ir y buscar.

Lo que la gente busca
Una línea de registro del servidor explicando el 1006, asumiendo que el servidor la envió
Lo que existe
Nada. El código fue generado en el navegador. El servidor puede no saber aún que la conexión se fue, y lo que la mató a menudo ni siquiera es el servidor

1006 por lo tanto no es una causa. Es la forma de la evidencia: algo entre los dos extremos dejó de retransmitir, y ninguno de los extremos llegó a despedirse.

Quién realmente cierra una conexión que nadie cerró

Una vez que aceptas que el navegador está reportando silencio, los sospechosos son las cosas que se sientan en el medio y tienen opiniones sobre conexiones inactivas.

Un proxy inverso o balanceador de carga con un timeout de inactividad es el más común. Esos timeouts son configurables y a menudo cortos, y un WebSocket que no lleva nada entre actualizaciones se ve exactamente como una conexión abandonada. El equipo de red corporativo hace lo mismo, más agresivamente. Un teléfono moviéndose de wifi a datos móviles cambia la dirección del cliente bajo un socket abierto, lo que lo cierra sin ceremonia. Las laptops suspendidas hacen lo mismo.

Nota lo que todos ellos tienen en común: tu código de aplicación es correcto, tu servidor está saludable, y nada está mal con los datos. El transporte se fue. Esa es también la razón por la que golpea a algunos usuarios constantemente y a otros nunca, lo que es la firma que envía un reporte de error marcado como “no se puede reproducir”.

La defensa estándar es un latido: envía algo pequeño en un temporizador, más a menudo que el timeout de inactividad más corto entre tú y tus usuarios. No arregla una conexión caída. Evita que una inactiva sea confundida con una muerta, que es un problema diferente que produce el mismo reporte.

Qué registrar, porque el evento close es el único testigo

El evento close ocurre una vez, en el navegador, y luego la página usualmente se reconecta y la evidencia se va. Cuatro campos vale la pena capturar en ese momento.

socket.addEventListener('close', (event) => {
  report({
    code: event.code,                      // 1006 significa que nadie envió un código
    wasClean: event.wasClean,              // true si la conexión se cerró limpiamente
    reason: event.reason,                  // usualmente vacío, y vacío es informativo
    secondsSinceLastMessage: (Date.now() - lastMessageAt) / 1000,
    reconnects: reconnectCount
  });
});

wasClean es la señal más barata de las cuatro: MDN la define como true “si la conexión se cerró limpiamente”, así que un false separa una conexión que terminó de una que fue terminada. Y secondsSinceLastMessage es el campo que atrapa un timeout de inactividad, porque el intervalo se agrupará alrededor del mismo valor para cada usuario afectado. Un cluster justo debajo de sesenta segundos no es una coincidencia, es un número configurado en algún lugar en la ruta.

Session Replay

Extensión Chrome gratuita. Un clic en la página que se está portando mal captura la captura de pantalla, la consola y el registro de red, y te entrega un enlace para pegar en el ticket.

Obtén la extensión

La pregunta que debes hacer a la persona que lo reportó

Una pregunta, y no es una técnica: ¿una recarga lo arregló?

Si lo hizo, los datos nunca fueron incorrectos y tu renderizado nunca fue incorrecto. Una conexión fresca fue todo lo que se necesitaba, lo que apunta al transporte y lejos de todo lo demás. Esa única respuesta elimina más que cualquier cantidad de adivinanza sobre su red, y pueden darla sin saber qué es un WebSocket.

El seguimiento que vale la pena preguntar es cuánto tiempo había estado abierta la pestaña. Una pestaña dejada abierta durante el almuerzo y una pestaña abierta hace un minuto son casos diferentes, y el reportero conoce la respuesta.

Qué sacar de ello

El fallo es silencioso por construcción. Nada lanza una excepción, el evento error no lleva detalle, y la página sigue renderizando la última cosa que se le dijo.

El código de cierre es todo el mensaje, y el más común es una invención local: 1006 es tu navegador diciendo que nada llegó, no tu servidor diciendo por qué se fue. Los códigos que nadie puede enviar son códigos que nadie puede explicar, así que deja de buscar la mitad del lado del servidor y comienza a buscar qué se sienta entre tú y el usuario.

La especificación en sí es RFC 6455, y los códigos de cierre con sus significados de una línea están en la página de MDN sobre CloseEvent.code.