Наконец-то приходит сообщение: страница перестала обновляться. Никакой ошибки не было. Цифры просто перестали меняться, а обновление страницы всё исправило.

Поэтому об этом редко кто-то говорит. Человек, который столкнулся с этим, получает рабочее решение в руках в течение нескольких секунд, поэтому большинство никогда вам об этом не рассказывают. Те, кто рассказывает, не могут описать ничего, кроме отсутствия.

Закрытое соединение - это не ошибка

Первое, что нужно переучить, это то, что это событие не приходит как ошибка запроса. Оно не приходит так.

WebSocket срабатывает с событием error, и MDN описывает его тип тремя словами: “Общее событие”. Под ним нет пользовательского интерфейса, что означает отсутствие кода, причины и статуса на объекте, который вы получите. Ничего не выбросит исключение, поэтому ничего не попадёт в try/catch. Соединение заканчивается, и единственное, что несёт информацию, это событие close, которое следует за ним.

Поэтому код закрытия не является деталью. Это всё сообщение целиком.

Коды, которые что-то означают

Есть четыре, которые стоит знать наизусть. MDN цитирует их напрямую.

  • 1000, нормальное закрытие: “Соединение успешно завершило цель, для которой оно было создано”. Нечего расследовать.
  • 1001, уходит: “Конечная точка уходит, либо из-за отказа сервера, либо потому что браузер переходит на другую страницу, которая открыла это соединение”.
  • 1011, внутренняя ошибка: “Сервер прерывает соединение, потому что столкнулся с неожиданным условием, которое помешало ему выполнить запрос”.
  • 1006, ненормальное закрытие - это тот, который вы действительно увидите, и об этом будет рассказываться в остальной части этой статьи.

Прочитайте 1001 дважды, потому что он охватывает два не связанных между собой события с одним номером. Отключение сервера и переход пользователя по ссылке оба производят его. Если вы считаете 1001 как инциденты, вы считаете людей, уходящих с вашего сайта, как инциденты, и это число будет отслеживать ваш трафик, а не вашу надёжность.

1011 - это противоположность, и это тот, на который стоит поставить оповещение. Это сервер, признающий неисправность в его собственных словах.

Код, который вы видите чаще всего, не мог прийти ни от кого

Вот та часть, которая почти никогда не написана, и это меняет то, как вы расследуете.

RFC 6455 определяет 1006 так:

1006 - это зарезервированное значение, и конечная точка НЕ ДОЛЖНА устанавливать его в качестве кода статуса в фрейме управления Close. Оно предназначено для использования в приложениях, ожидающих кода состояния, чтобы указать, что соединение было закрыто ненормально, например, без отправки или получения фрейма Close.

1005 несёт то же самое запрещение: “1005 - это зарезервированное значение, и конечная точка НЕ ДОЛЖНА устанавливать его в качестве кода статуса в фрейме управления Close”.

Поэтому ни один из них не может быть отправлен. Не вашим сервером, не браузером, не чем-либо посредине. Они запрещены на проводе. Когда ваш обработчик читает 1006, он читает число, которое ваш собственный браузер придумал локально, чтобы описать тот факт, что соединение закончилось и никогда не пришёл фрейм закрытия.

Это более сильное утверждение, чем оно выглядит. Это означает, что нет соответствующего события где-то ещё, куда можно пойти и найти.

Что люди ищут
Строку логов сервера, объясняющую 1006, в предположении, что сервер его отправил
Что существует
Ничего. Код был сгенерирован в браузере. Сервер может даже не знать, что соединение исчезло, и то, что его убило, часто вообще не сервер

1006 поэтому не является причиной. Это форма доказательства: что-то между двумя концами перестало передавать, и ни один конец не смог попрощаться.

Кто на самом деле закрывает соединение, которое никто не закрывал

Как только вы признаёте, что браузер сообщает о молчании, подозреваемыми становятся вещи, которые сидят посередине и имеют мнения о простаивающих соединениях.

Обратный прокси или балансировщик нагрузки с таймаутом неактивности - это распространённый случай. Эти таймауты настраиваются и часто коротки, и WebSocket, не передающий ничего между обновлениями, выглядит ровно как заброшенное соединение. Корпоративное сетевое оборудование делает то же самое, более агрессивно. Телефон, переходящий с wifi на мобильные данные, меняет адрес клиента под открытым сокетом, что заканчивает его без церемоний. Ноутбуки при переходе в режим сна делают то же самое.

Обратите внимание на то, что у каждого из них есть общее: ваш код приложения верен, ваш сервер здоров, и с данными ничего не так. Транспорт исчез. Это также объясняет, почему это происходит у некоторых пользователей постоянно, а у других никогда, что является признаком, отправляющим отчёт об ошибке с меткой “невозможно воспроизвести”.

Стандартная защита - это сердцебиение: отправляйте что-нибудь маленькое на таймере, чаще, чем самый короткий таймаут неактивности между вами и вашими пользователями. Это не исправляет разорванное соединение. Это предотвращает ошибку простаивающего соединения, принимаемого за неработающее, что является другой проблемой, которая производит тот же отчёт.

Что записывать, потому что событие закрытия - это единственный свидетель

Событие закрытия происходит один раз в браузере, а затем страница обычно переподключается и доказательство исчезает. Четыре поля стоит захватить в этот момент.

socket.addEventListener('close', (event) => {
  report({
    code: event.code,                      // 1006 означает, что никто не отправил код
    wasClean: event.wasClean,              // true, если соединение закрылось чисто
    reason: event.reason,                  // обычно пусто, и пусто информативно
    secondsSinceLastMessage: (Date.now() - lastMessageAt) / 1000,
    reconnects: reconnectCount
  });
});

wasClean - это самый дешёвый сигнал из четырёх: MDN определяет его как true “если соединение закрылось чисто”, поэтому false разделяет соединение, которое закончилось, от того, которое было закрыто. И secondsSinceLastMessage - это поле, которое ловит таймаут неактивности, потому что интервал будет группироваться вокруг одного значения для каждого затронутого пользователя. Кластер чуть менее шестидесяти секунд - это не совпадение, это настроенное число где-то на пути.

Session Replay

Бесплатное расширение Chrome. Один щелчок на странице, которая неправильно ведёт себя, захватывает снимок экрана, консоль и журнал сети, и передаёт вам ссылку для вставки в тикет.

Получить расширение

Вопрос, который нужно задать человеку, который об этом сообщил

Один вопрос, и это не технический: обновление страницы это исправило?

Если да, то данные никогда не были неправильными и ваш рендеринг никогда не был неправильным. Свежее соединение было всё, что было нужно, что указывает на транспорт и уходит от всего остального. Этот один ответ устраняет больше, чем любое количество угадывания их сети, и они могут дать его, не зная, что такое WebSocket.

Следующий вопрос, стоящий того, чтобы его задать, это как долго была открыта страница. Вкладка, оставленная открытой на обед, и вкладка, открытая минуту назад, это разные случаи, и докладчик знает ответ.

Что взять из этого

Отказ молчалив по конструкции. Ничего не выбросит исключение, событие error не несёт деталей, и страница продолжает отображать последнее, что ей было сказано.

Код закрытия - это всё сообщение, и самый распространённый из них - это локальное изобретение: 1006 это ваш браузер, говорящий, что ничего не пришло, а не ваш сервер, говорящий, почему он ушёл. Коды, которые никто не может отправить, это коды, которые никто не может объяснить, поэтому перестаньте искать серверную половину и начните искать то, что находится между вами и пользователем.

Спецификация сама по себе - это RFC 6455, и коды закрытия с их однострочными значениями находятся на странице CloseEvent.code MDN.