Ошибки подписки

Подписка в STOMP.js опирается на модель маршрутизации сообщений через брокер и является одним из самых частых источников нестабильного поведения в реальных приложениях. Ошибки подписки редко проявляются как единичные сбои — чаще они выражаются в потере сообщений, дублировании доставки, «тихих» отказах или отсутствии событий без явного уведомления со стороны клиента.

Подписка в STOMP.js создаётся поверх активного соединения с брокером и существует до момента её явного удаления или разрыва соединения. На уровне протокола STOMP подписка представляет собой команду SUBSCRIBE с указанием destination и параметров обработки.

Ключевая особенность заключается в том, что брокер не всегда обязан подтверждать успешное создание подписки. В зависимости от реализации брокера (RabbitMQ, ActiveMQ, Apollo, Spring WebSocket Broker), поведение может различаться:

  • некоторые брокеры возвращают frame receipt при включённом ack;
  • другие создают подписку «молча»;
  • третьи откладывают регистрацию до завершения handshake WebSocket.

Это приводит к тому, что часть ошибок подписки проявляется только косвенно.

Ошибки тайминга подписки

Одной из наиболее распространённых проблем является попытка подписаться до полной готовности соединения.

В STOMP.js соединение проходит несколько этапов:

  1. WebSocket установлен
  2. STOMP CONNECT отправлен
  3. брокер отвечает CONNECTED
  4. только после этого допустим SUBSCRIBE

Если SUBSCRIBE отправляется раньше, библиотека может:

  • поставить команду в очередь (если реализовано буферизование);
  • либо потерять её при разрыве соединения;
  • либо выполнить с ошибкой без явного callback.

Типичный сценарий ошибки:

client.activate();

client.subscribe('/topic/updates', message => {
  console.log(message.body);
});

Если subscribe вызывается до события onConnect, подписка может не зарегистрироваться в брокере.

Корректная модель предполагает привязку к состоянию соединения:

client.onConn ect = () => {
  client.subscribe('/topic/updates', message => {
    console.log(message.body);
  });
};

Потеря подписки при реконнекте

STOMP.js поддерживает автоматическое переподключение, однако подписки при этом не всегда восстанавливаются автоматически.

Типичная ошибка проявляется следующим образом:

  • соединение разрывается
  • клиент переподключается
  • сообщения больше не приходят

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

При реконнекте необходимо заново регистрировать все destination.

Распространённая архитектурная ошибка — хранение подписки как объекта без логики восстановления:

let sub;

client.onConn ect = () => {
  sub = client.subscribe('/topic/updates', handler);
};

После reconnection sub остаётся ссылкой на старую подписку, которая больше не активна.

Корректный подход — централизованный реестр подписок:

const subscriptions = [];

function registerSubscriptions(client) {
  subscriptions.push(
    client.subscribe('/topic/updates', handler)
  );
}

client.onConn ect = () => {
  registerSubscriptions(client);
};

Ошибки destination

Неверно указанный destination — одна из самых «тихих» ошибок. Брокер часто не возвращает явного отказа, особенно если маршрут просто не существует.

Основные причины:

  • опечатка в пути (/topic/update вместо /topic/updates)
  • несоответствие формата (queue vs topic)
  • отсутствие конфигурации маршрута на сервере

В STOMP.js это проявляется как отсутствие сообщений без ошибок в консоли.

Некоторые брокеры (например, Spring STOMP) могут логировать проблему на сервере, но не возвращать её клиенту.

Проблемы с правами доступа

При использовании брокеров с авторизацией подписка может быть отклонена из-за отсутствия прав.

Типовые сценарии:

  • пользователь не имеет доступа к destination
  • токен истёк до подписки
  • заголовки авторизации не переданы при SUBSCRIBE

STOMP.js позволяет передавать headers:

client.subscribe(
  '/topic/updates',
  handler,
  { Authorization: 'Bearer token' }
);

Однако ошибка проявляется не всегда явно. Некоторые брокеры просто не доставляют сообщения, не отправляя ERROR frame.

Ошибки обработки ACK

При использовании режимов ack: ‘client’ или ack: ‘client-individual’ возможны сбои, связанные с некорректным подтверждением сообщений.

Хотя это формально относится к обработке сообщений, последствия часто выглядят как ошибка подписки:

  • сообщения перестают приходить
  • брокер считает сообщения необработанными
  • канал блокируется

Пример проблемного сценария:

client.subscribe('/queue/tasks', message => {
  process(message.body);
  // отсутствие ack
}, { ack: 'client' });

При отсутствии message.ack() брокер может приостановить доставку.

Правильная обработка:

client.subscribe('/queue/tasks', message => {
  try {
    process(message.body);
    message.ack();
  } catch (e) {
    message.nack();
  }
}, { ack: 'client' });

Конкурентные подписки и дублирование

При повторной инициализации подписки без очистки старой возникают дубликаты обработчиков.

Это приводит к эффекту:

  • одно сообщение обрабатывается несколько раз
  • нагрузка на клиент растёт линейно
  • логика бизнес-слоя становится недетерминированной

Причина часто кроется в повторном вызове subscribe при каждом onConnect без контроля состояния:

client.onConn ect = () => {
  client.subscribe('/topic/updates', handler);
  client.subscribe('/topic/updates', handler);
};

Каждый вызов создаёт отдельную подписку на брокере.

Ошибки heartbeat и разрывы подписки

Heartbeat в STOMP используется для контроля живости соединения. При некорректной настройке возможны скрытые разрывы:

  • клиент считает соединение активным
  • брокер уже разорвал сессию
  • подписки фактически не существуют

Особенно часто это происходит при:

  • слишком большом interval
  • прокси, обрывающих idle-соединения
  • несовпадении heartbeat client/server

Результат — отсутствие сообщений без явного события error.

Обработка ERROR frame

Некоторые брокеры отправляют STOMP frame типа ERROR при проблемах подписки. STOMP.js позволяет перехватывать такие события через:

client.onStompEr ror = frame => {
  console.error(frame.headers['message']);
  console.error(frame.body);
};

Однако важно учитывать, что:

  • не все ошибки подписки доходят как ERROR frame
  • часть ошибок остаётся на уровне отсутствия сообщений
  • некоторые брокеры используют нестандартные расширения

Состояние гонки при массовых подписках

При одновременной подписке на десятки destination возможна ситуация гонки:

  • соединение установлено
  • подписки отправляются параллельно
  • часть frames теряется при перегрузке канала

Это особенно заметно в браузерных WebSocket реализациях при слабом соединении.

Решение обычно связано с сериализацией подписок:

async function subscribeAll(client, list) {
  for (const dest of list) {
    client.subscribe(dest, handler);
    await new Promise(r => setTimeout(r, 10));
  }
}

Неконсистентность состояния клиента

STOMP.js не хранит централизованное состояние подписок. Это приводит к следующим классам ошибок:

  • утечка подписок при повторных соединениях
  • невозможность корректного unsubscribe
  • расхождение между клиентским и серверным состоянием

Классическая проблема:

const sub = client.subscribe('/topic/updates', handler);
client.deactivate();
// sub.unsubscribe() не вызван

После переподключения создаётся новая подписка, старая остаётся в памяти брокера до истечения сессии.

Итоговая природа ошибок подписки

Ошибки подписки в STOMP.js почти всегда являются следствием несоответствия между тремя слоями:

  • жизненным циклом WebSocket соединения
  • состоянием брокера
  • логикой клиента

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