В системах обмена сообщениями ошибка в структуре сообщения способна привести к цепочке проблем:
При работе со STOMP.js особенно важно валидировать данные,
поступающие через подписки (SUBSCRIBE) и пользовательские
каналы. WebSocket-соединение поддерживает постоянный поток сообщений,
поэтому ошибка в одном пакете способна многократно повторяться и
вызывать нестабильность всей системы.
Валидация должна выполняться:
В STOMP.js входящие данные обычно поступают через callback-функции подписок:
client.subscribe('/topic/orders', (message) => {
console.log(message.body);
});
Источниками потенциально некорректных данных являются:
Даже если сервер считается доверенным, клиентская проверка остаётся обязательной.
Первичная валидация начинается с проверки самого сообщения.
client.subscribe('/topic/orders', (message) => {
if (!message) {
return;
}
if (!message.body) {
return;
}
console.log(message.body);
});
Без подобной проверки возможны ошибки:
Cannot read properties of undefined
Особенно это важно при реконнектах и нестабильных соединениях.
Большинство STOMP-приложений передаёт JSON.
Нельзя напрямую вызывать JSON.parse() без обработки
исключений.
Небезопасный вариант:
const data = JSON.parse(message.body);
Безопасный вариант:
function safeParseJSON(text) {
try {
return JSON.parse(text);
} catch (error) {
return null;
}
}
client.subscribe('/topic/orders', (message) => {
const data = safeParseJSON(message.body);
if (!data) {
console.error('Некорректный JSON');
return;
}
console.log(data);
});
После успешного парсинга необходимо валидировать структуру объекта.
Пример ожидаемого сообщения:
{
"id": 15,
"status": "created",
"amount": 1200
}
Проверка:
function validateOrder(data) {
if (typeof data !== 'object') {
return false;
}
if (typeof data.id !== 'number') {
return false;
}
if (typeof data.status !== 'string') {
return false;
}
if (typeof data.amount !== 'number') {
return false;
}
return true;
}
Использование:
client.subscribe('/topic/orders', (message) => {
const data = safeParseJSON(message.body);
if (!validateOrder(data)) {
console.error('Ошибка структуры данных');
return;
}
processOrder(data);
});
Даже корректный JSON может не содержать нужных полей.
Проблемное сообщение:
{
"status": "created"
}
Проверка обязательных свойств:
function hasRequiredFields(data, fields) {
return fields.every(field => field in data);
}
Использование:
if (!hasRequiredFields(data, ['id', 'status', 'amount'])) {
console.error('Отсутствуют обязательные поля');
return;
}
В JavaScript типы могут приходить в неожиданном виде:
{
"id": "15",
"amount": "500"
}
Если логика ожидает числа, необходимо явно проверять типы.
function isNumber(value) {
return typeof value === 'number' && !isNaN(value);
}
Проверка:
if (!isNumber(data.amount)) {
console.error('amount должен быть числом');
return;
}
Даже корректный тип не гарантирует корректное значение.
Некорректные примеры:
{
"amount": -100000
}
{
"amount": 999999999999999999
}
Проверка диапазона:
function validateAmount(amount) {
return amount >= 0 && amount <= 1000000;
}
Строки могут содержать:
Проверка:
function validateStatus(status) {
const allowed = [
'created',
'paid',
'cancelled'
];
return allowed.includes(status);
}
Одна из важных мер безопасности — ограничение размера входящего сообщения.
Опасный сценарий:
50 МБ JSON через WebSocket
Подобное сообщение способно:
Проверка:
const MAX_MESSAGE_SIZE = 1024 * 100;
client.subscribe('/topic/orders', (message) => {
if (message.body.length > MAX_MESSAGE_SIZE) {
console.error('Сообщение слишком большое');
return;
}
});
STOMP-сообщения поддерживают заголовки.
Например:
client.publish({
destination: '/topic/orders',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify(data)
});
Проверка на принимающей стороне:
client.subscribe('/topic/orders', (message) => {
const contentType = message.headers['content-type'];
if (contentType !== 'application/json') {
console.error('Неверный content-type');
return;
}
});
Часто сообщения содержат фиксированные состояния.
Пример:
{
"status": "unknown_status"
}
Проверка:
const allowedStatuses = new Set([
'created',
'processing',
'done',
'failed'
]);
if (!allowedStatuses.has(data.status)) {
console.error('Недопустимый статус');
return;
}
Timestamp-поля должны проверяться отдельно.
Пример:
{
"createdAt": "invalid-date"
}
Проверка:
function isValidDate(date) {
return !isNaN(Date.parse(date));
}
STOMP.js часто используется в чатах и realtime-интерфейсах.
Опасный пример:
{
"message": "<script>alert(1)</script>"
}
Нельзя вставлять входящие данные напрямую через
innerHTML.
Опасно:
container.innerHTML = data.message;
Безопаснее:
container.textContent = data.message;
Если HTML всё же необходим, требуется очистка.
Пример с DOMPurify:
import DOMPurify from 'dompurify';
const cleanHTML = DOMPurify.sanitize(data.message);
В крупных приложениях ручные проверки становятся неудобными.
Популярные библиотеки:
Пример схемы:
import { z } from 'zod';
const OrderSchema = z.object({
id: z.number(),
status: z.string(),
amount: z.number().min(0)
});
Проверка:
client.subscribe('/topic/orders', (message) => {
const data = safeParseJSON(message.body);
const result = OrderSchema.safeParse(data);
if (!result.success) {
console.error(result.error);
return;
}
processOrder(result.data);
});
Ajv особенно полезен для JSON Schema.
Схема:
const schema = {
type: 'object',
properties: {
id: { type: 'number' },
status: { type: 'string' },
amount: { type: 'number' }
},
required: ['id', 'status', 'amount']
};
Проверка:
import Ajv from 'ajv';
const ajv = new Ajv();
const validate = ajv.compile(schema);
if (!validate(data)) {
console.error(validate.errors);
}
Сообщения часто содержат списки объектов.
Пример:
{
"items": [
{
"id": 1
}
]
}
Проверка:
if (!Array.isArray(data.items)) {
return;
}
Валидация элементов:
for (const item of data.items) {
if (typeof item.id !== 'number') {
return;
}
}
Глубоко вложенные структуры требуют отдельной проверки.
Пример:
{
"user": {
"profile": {
"name": "Alex"
}
}
}
Безопасная проверка:
if (
!data.user ||
!data.user.profile ||
typeof data.user.profile.name !== 'string'
) {
return;
}
Вредоносный JSON может содержать:
{
"__proto__": {
"admin": true
}
}
Проблема особенно опасна при merge-операциях.
Небезопасно:
Object.assign(target, data);
Защита:
function hasDangerousKeys(obj) {
return [
'__proto__',
'constructor',
'prototype'
].some(key => key in obj);
}
После проверки данные часто приводятся к единому формату.
Пример:
function normalizeOrder(data) {
return {
id: Number(data.id),
status: data.status.trim(),
amount: Number(data.amount)
};
}
При использовании клиентских подтверждений
(client-individual) особенно важно валидировать данные до
ACK.
client.subscribe('/queue/orders', (message) => {
const data = safeParseJSON(message.body);
if (!validateOrder(data)) {
console.error('Некорректное сообщение');
return;
}
processOrder(data);
message.ack();
}, {
ack: 'client-individual'
});
Если ACK отправить раньше проверки, брокер удалит сообщение даже при ошибке обработки.
Некоторые системы используют retry-механизмы.
Пример:
let retries = 0;
function processMessage(message) {
try {
const data = JSON.parse(message.body);
validateOrder(data);
} catch (error) {
retries++;
if (retries < 3) {
setTimeout(() => {
processMessage(message);
}, 1000);
}
}
}
Ошибка в одном callback не должна ломать всё соединение.
Небезопасно:
client.subscribe('/topic/orders', (message) => {
const data = JSON.parse(message.body);
processOrder(data);
});
Безопаснее:
client.subscribe('/topic/orders', (message) => {
try {
const data = JSON.parse(message.body);
processOrder(data);
} catch (error) {
console.error(error);
}
});
Полезно фиксировать:
Пример:
function logValidationError(error, message) {
console.error({
error,
subscription: message.headers.destination,
messageId: message.headers['message-id'],
timestamp: Date.now()
});
}
В некоторых случаях сообщение лучше полностью игнорировать.
Признаки подозрительности:
Даже валидные сообщения могут перегружать приложение.
Пример ограничения:
let counter = 0;
setInterval(() => {
counter = 0;
}, 1000);
client.subscribe('/topic/events', (message) => {
counter++;
if (counter > 100) {
return;
}
processEvent(message);
});
В больших проектах удобно создавать единый валидатор.
Пример архитектуры:
class MessageValidator {
validate(message) {
const data = this.parse(message.body);
this.validateSchema(data);
this.validateSecurity(data);
return data;
}
parse(body) {
return JSON.parse(body);
}
validateSchema(data) {
}
validateSecurity(data) {
}
}
Использование:
const validator = new MessageValidator();
client.subscribe('/topic/orders', (message) => {
try {
const data = validator.validate(message);
processOrder(data);
} catch (error) {
console.error(error);
}
});
Некоторые брокеры могут передавать бинарные payload.
Проверка:
if (typeof message.body !== 'string') {
return;
}
Полезно проверять тип события.
Пример:
{
"event": "user_created"
}
Проверка:
const allowedEvents = [
'user_created',
'user_deleted',
'user_updated'
];
if (!allowedEvents.includes(data.event)) {
return;
}
Realtime-системы иногда требуют контроля порядка.
Пример:
{
"sequence": 150
}
Проверка:
if (data.sequence <= lastSequence) {
return;
}
lastSequence = data.sequence;
Проверка идентификаторов:
function isUUID(value) {
const regex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return regex.test(value);
}
Пример:
function isValidURL(value) {
try {
new URL(value);
return true;
} catch {
return false;
}
}
Иногда требуется серверная проверка.
Пример:
async function validateUser(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.ok;
}
Использование:
client.subscribe('/topic/orders', async (message) => {
const data = JSON.parse(message.body);
const exists = await validateUser(data.userId);
if (!exists) {
return;
}
});