Проблемы с кодировкой

Проблемы с кодировкой в приложениях на базе STOMP.js возникают при передаче текстовых сообщений между браузером, WebSocket-сервером и брокером сообщений. Наиболее распространённые симптомы:

  • появление «кракозябр» вместо текста;
  • повреждение кириллицы;
  • ошибки JSON.parse;
  • обрезка строк;
  • некорректная обработка emoji;
  • нарушение структуры UTF-8;
  • несовместимость между backend и frontend;
  • ошибки сериализации бинарных данных.

STOMP.js работает поверх WebSocket и использует текстовые фреймы STOMP-протокола. Внутри этих фреймов данные передаются как строки или бинарные массивы. Любое несовпадение кодировок между клиентом и сервером приводит к повреждению содержимого.


Как STOMP.js обрабатывает текст

Внутри STOMP-фрейма тело сообщения представляет собой строку:

SEND
destination:/topic/chat
content-type:text/plain

Привет мир
^@

По умолчанию STOMP.js предполагает использование UTF-8. Однако проблемы начинаются в нескольких случаях:

  • сервер отправляет ISO-8859-1;
  • backend сериализует данные в Windows-1251;
  • бинарные данные ошибочно трактуются как текст;
  • отсутствует content-type;
  • WebSocket-сервер меняет кодировку;
  • брокер не указывает charset.

Типичная проблема с кириллицей

Наиболее распространённый случай — повреждение русского текста.

Вместо:

Привет

клиент получает:

Привет

Причина — UTF-8 интерпретируется как Latin-1.


Ошибка на стороне backend

Например, Java-сервер отправляет сообщение:

message.getBytes("CP1251")

а браузер ожидает UTF-8.

В результате STOMP.js получает повреждённую строку.


Правильная настройка UTF-8

Node.js

client.publish({
    destination: '/topic/chat',
    body: JSON.stringify(data),
    headers: {
        'content-type': 'application/json;charset=UTF-8'
    }
});

Spring Boot

mappingJackson2MessageConverter.setDefaultCharset(StandardCharsets.UTF_8);

Express + ws

ws.send(JSON.stringify(data));

Node.js по умолчанию использует UTF-8, поэтому ручная перекодировка обычно не требуется.


Влияние заголовка content-type

STOMP.js ориентируется на заголовки сообщения.

Если сервер отправляет:

content-type:text/plain

кодировка считается неопределённой.

Правильный вариант:

content-type:text/plain;charset=UTF-8

Для JSON:

content-type:application/json;charset=UTF-8

Проблемы с JSON

Повреждение JSON-структуры

Если кодировка нарушена, JSON становится невалидным.

Пример:

{
    "message":"Привет"
}

После неправильной декодировки:

{
    "message":"Привет"
}

Иногда повреждаются кавычки и escape-последовательности, что приводит к:

JSON.parse(message.body)

ошибке:

Unexpected token

Безопасный разбор сообщений

client.subscribe('/topic/messages', message => {
    try {
        const data = JSON.parse(message.body);
        console.log(data);
    } catch (e) {
        console.error('Ошибка декодирования', e);
        console.log(message.body);
    }
});

Бинарные данные и кодировка

STOMP.js поддерживает бинарные payload.

Ошибка возникает, когда бинарные данные пытаются читать как текст.


Неправильный вариант

client.publish({
    destination: '/topic/file',
    body: binaryData
});

Если binaryData — ArrayBuffer, сервер может интерпретировать его как строку.


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

client.publish({
    destination: '/topic/file',
    binaryBody: uint8Array,
    headers: {
        'content-type': 'application/octet-stream'
    }
});

Использование TextEncoder и TextDecoder

Для контроля кодировки применяются встроенные API браузера.


Кодирование UTF-8

const encoder = new TextEncoder();

const bytes = encoder.encode('Привет');

Декодирование UTF-8

const decoder = new TextDecoder('utf-8');

const text = decoder.decode(bytes);

Поддержка других кодировок

const decoder = new TextDecoder('windows-1251');

Но браузерная поддержка зависит от платформы.


Проблемы с emoji

Emoji занимают несколько байтов в UTF-8.

Например:

?

может занимать 4 байта.

Если сервер режет payload по символам вместо байтов, сообщение повреждается.


Ошибка длины content-length

Некоторые backend-приложения вычисляют:

body.length()

вместо:

body.getBytes(StandardCharsets.UTF_8).length

Для кириллицы и emoji значения различаются.


Последствия неправильного content-length

  • обрезка сообщения;
  • потеря части UTF-8 символа;
  • invalid continuation byte;
  • повреждение JSON;
  • разрыв STOMP-фрейма.

Проблемы content-length в STOMP

STOMP использует заголовок:

content-length

Если длина указана неверно, брокер неправильно считывает тело сообщения.


Пример ошибки

Текст:

Привет

Содержит:

  • 6 символов;
  • 12 байтов UTF-8.

Если сервер отправит:

content-length:6

фрейм будет обрезан.


Корректное вычисление длины

Node.js

Buffer.byteLength(body, 'utf8')

Java

body.getBytes(StandardCharsets.UTF_8).length

Проблемы с SockJS

SockJS иногда меняет формат передачи данных.

В старых конфигурациях возможны:

  • двойная сериализация;
  • escape Unicode;
  • преобразование бинарных данных в строку;
  • потеря charset.

Пример повреждения Unicode

Исходный текст:

Привет

После промежуточной сериализации:

\u041f\u0440\u0438\u0432\u0435\u0442

После некорректного повторного декодирования:

\\u041f\\u0440...

Проверка реальных данных WebSocket

Для диагностики удобно использовать DevTools браузера.


Chrome DevTools

Раздел:

Network → WS → Frames

Позволяет увидеть:

  • реальные STOMP-фреймы;
  • байтовое содержимое;
  • заголовки;
  • content-length;
  • бинарный формат;
  • повреждение UTF-8.

Разница между строками и байтами

Одна из самых опасных ошибок — путать:

  • количество символов;
  • количество байтов.

Пример

const text = 'Привет';

Длина строки

text.length

Результат:

6

Длина UTF-8

new TextEncoder().encode(text).length

Результат:

12

Проблемы сериализации JavaScript

Иногда кодировка ломается при ручной сериализации.


Ошибочный вариант

body: data.toString()

Если data — объект:

[object Object]

Правильный вариант

body: JSON.stringify(data)

Повреждение Base64

Иногда бинарные данные передаются как Base64.


Ошибка Unicode

btoa('Привет')

Вызывает:

InvalidCharacterError

Потому что btoa работает только с Latin-1.


Корректное кодирование Base64

const bytes = new TextEncoder().encode('Привет');

const binary = Array.from(bytes)
    .map(b => String.fromCharCode(b))
    .join('');

const base64 = btoa(binary);

Диагностика кодировки в STOMP.js

Проверка заголовков

console.log(message.headers);

Важно анализировать:

content-type
content-length

Проверка бинарного режима

console.log(message.binaryBody);

Проверка реальных байтов

const bytes = new Uint8Array(message.binaryBody);

console.log(bytes);

Различия между брокерами

Разные брокеры по-разному работают с кодировкой.


RabbitMQ

Обычно корректно использует UTF-8, но проблемы возможны при:

  • сторонних плагинах;
  • AMQP-конвертации;
  • неправильных content-type.

ActiveMQ

Может автоматически конвертировать текстовые сообщения.

Иногда это ломает бинарные payload.


Apollo

В старых версиях встречались проблемы с UTF-8 и content-length.


Проблемы двойного UTF-8

Иногда данные кодируются дважды.


Пример

Исходный текст:

Привет

После первого UTF-8:

Пр

После повторного UTF-8:

ПÐÂ...

Причины

  • повторный decodeURIComponent;
  • лишний TextDecoder;
  • двойная сериализация;
  • middleware повторно кодирует payload.

Ошибки URL-кодирования

Иногда payload проходит через:

encodeURIComponent()

а затем:

decodeURIComponent()

в неправильном порядке.


Повреждённый текст

%D0%9F%D1%80...

может остаться нераскодированным.


Кодировка в STOMP over SockJS

SockJS может использовать:

  • xhr-streaming;
  • xhr-polling;
  • iframe;
  • websocket.

Некоторые транспорты исторически хуже работали с Unicode.

Особенно это проявлялось в старых браузерах.


Безопасная универсальная схема

Frontend

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws'
});

Отправка

client.publish({
    destination: '/topic/chat',
    body: JSON.stringify(data),
    headers: {
        'content-type': 'application/json;charset=UTF-8'
    }
});

Получение

client.subscribe('/topic/chat', message => {
    const data = JSON.parse(message.body);

    console.log(data);
});

Рекомендации по предотвращению проблем

Всегда использовать UTF-8

Другие кодировки в WebSocket/STOMP создают большое количество несовместимостей.


Указывать charset

charset=UTF-8

должен присутствовать в content-type.


Не вычислять длину строки вручную

Только байтовая длина.


Не смешивать бинарные и текстовые payload

Для файлов использовать:

binaryBody

Не использовать устаревшие кодировки

Избегать:

  • CP1251;
  • KOI8-R;
  • ISO-8859-1.

Проверять промежуточные proxy

Некоторые reverse proxy меняют заголовки и payload.

Особенно:

  • nginx;
  • HAProxy;
  • websocket gateway;
  • cloud proxy.

Проверка UTF-8 вручную

Валидация

function isUtf8(bytes) {
    try {
        new TextDecoder('utf-8', { fatal: true }).decode(bytes);

        return true;
    } catch {
        return false;
    }
}

Перекодировка повреждённого текста

Иногда возможно восстановление.


Пример

function fixUtf8(str) {
    return decodeURIComponent(escape(str));
}

Но метод считается устаревшим и работает не всегда.


Работа с бинарным JSON

Для высоконагруженных систем JSON иногда передают как UTF-8 байты.


Отправка

const bytes = new TextEncoder()
    .encode(JSON.stringify(data));

client.publish({
    destination: '/topic/data',
    binaryBody: bytes
});

Получение

client.subscribe('/topic/data', message => {
    const text = new TextDecoder()
        .decode(message.binaryBody);

    const data = JSON.parse(text);

    console.log(data);
});

Симптомы неправильной кодировки

Повреждение кириллицы

Привет

Ошибка JSON

Unexpected token

Обрезанные строки

Прив�

Потеря emoji

��

Ошибки декодирования

Malformed UTF-8 data

Логирование байтов для диагностики

const bytes = new Uint8Array(message.binaryBody);

console.log(
    Array.from(bytes)
        .map(v => v.toString(16))
);

Позволяет увидеть реальное содержимое UTF-8 пакета.


Проблемы старых браузеров

Старые браузеры:

  • Internet Explorer;
  • старые Android WebView;
  • ранние Safari;

могут иметь:

  • неполную поддержку UTF-8;
  • проблемы TextDecoder;
  • ошибки бинарного WebSocket API.

Архитектурные причины проблем кодировки

Наиболее опасны системы, где присутствуют:

  • несколько proxy;
  • несколько языков программирования;
  • legacy backend;
  • старые очереди сообщений;
  • AMQP/STOMP bridge;
  • бинарные gateway;
  • ручная сериализация сообщений.

Каждый промежуточный слой способен повредить UTF-8.


Наиболее безопасная стратегия

На практике наиболее стабильной считается схема:

  • UTF-8 везде;
  • JSON только через JSON.stringify;
  • обязательный charset;
  • бинарные данные только через binaryBody;
  • автоматическая сериализация;
  • отказ от legacy-кодировок;
  • контроль content-length в байтах;
  • проверка WebSocket-фреймов через DevTools;
  • единый формат сообщений между всеми сервисами.