Подписка в STOMP.js опирается на модель маршрутизации сообщений через брокер и является одним из самых частых источников нестабильного поведения в реальных приложениях. Ошибки подписки редко проявляются как единичные сбои — чаще они выражаются в потере сообщений, дублировании доставки, «тихих» отказах или отсутствии событий без явного уведомления со стороны клиента.
Подписка в STOMP.js создаётся поверх активного соединения с брокером и существует до момента её явного удаления или разрыва соединения. На уровне протокола STOMP подписка представляет собой команду SUBSCRIBE с указанием destination и параметров обработки.
Ключевая особенность заключается в том, что брокер не всегда обязан подтверждать успешное создание подписки. В зависимости от реализации брокера (RabbitMQ, ActiveMQ, Apollo, Spring WebSocket Broker), поведение может различаться:
Это приводит к тому, что часть ошибок подписки проявляется только косвенно.
Одной из наиболее распространённых проблем является попытка подписаться до полной готовности соединения.
В STOMP.js соединение проходит несколько этапов:
Если SUBSCRIBE отправляется раньше, библиотека может:
Типичный сценарий ошибки:
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 — одна из самых «тихих» ошибок. Брокер часто не возвращает явного отказа, особенно если маршрут просто не существует.
Основные причины:
В STOMP.js это проявляется как отсутствие сообщений без ошибок в консоли.
Некоторые брокеры (например, Spring STOMP) могут логировать проблему на сервере, но не возвращать её клиенту.
При использовании брокеров с авторизацией подписка может быть отклонена из-за отсутствия прав.
Типовые сценарии:
STOMP.js позволяет передавать headers:
client.subscribe(
'/topic/updates',
handler,
{ Authorization: 'Bearer token' }
);
Однако ошибка проявляется не всегда явно. Некоторые брокеры просто не доставляют сообщения, не отправляя ERROR frame.
При использовании режимов 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 в STOMP используется для контроля живости соединения. При некорректной настройке возможны скрытые разрывы:
Особенно часто это происходит при:
Результат — отсутствие сообщений без явного события error.
Некоторые брокеры отправляют STOMP frame типа ERROR при проблемах подписки. STOMP.js позволяет перехватывать такие события через:
client.onStompEr ror = frame => {
console.error(frame.headers['message']);
console.error(frame.body);
};
Однако важно учитывать, что:
При одновременной подписке на десятки destination возможна ситуация гонки:
Это особенно заметно в браузерных WebSocket реализациях при слабом соединении.
Решение обычно связано с сериализацией подписок:
async function subscribeAll(client, list) {
for (const dest of list) {
client.subscribe(dest, handler);
await new Promise(r => setTimeout(r, 10));
}
}
STOMP.js не хранит централизованное состояние подписок. Это приводит к следующим классам ошибок:
Классическая проблема:
const sub = client.subscribe('/topic/updates', handler);
client.deactivate();
// sub.unsubscribe() не вызван
После переподключения создаётся новая подписка, старая остаётся в памяти брокера до истечения сессии.
Ошибки подписки в STOMP.js почти всегда являются следствием несоответствия между тремя слоями:
STOMP-протокол не предоставляет строгой гарантии доставки управляющих событий подписки, поэтому устойчивость системы определяется архитектурой управления подписками, а не самим механизмом subscribe.