Короткий ответ: WebSocket не работает как fetch, где вызвал функцию и сразу получил конкретный ответ. Сообщения приходят асинхронно и могут прийти не в том порядке, в котором вы их отправили. Чтобы вернуть ответ из очереди сообщений, добавьте каждому запросу requestId, храните ожидающие запросы в Map, а при получении ответа находите нужный requestId и завершайте соответствующий Promise
Клиентский request-response поверх WebSocket
class WebSocketRpcClient {
constructor(socket) {
this.socket = socket;
this.pending = new Map();
this.socket.addEventListener("message", (event) => {
this.handleMessage(event.data);
});
}
request(type, payload, timeoutMs = 5000) {
const requestId = crypto.randomUUID();
const message = {
type,
requestId,
payload,
};
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pending.delete(requestId);
reject(new Error(`WebSocket request timeout: ${type}`));
}, timeoutMs);
this.pending.set(requestId, { resolve, reject, timeout });
this.socket.send(JSON.stringify(message));
});
}
handleMessage(raw) {
const message = JSON.parse(raw);
if (!message.requestId) {
return;
}
const pending = this.pending.get(message.requestId);
if (!pending) {
return;
}
clearTimeout(pending.timeout);
this.pending.delete(message.requestId);
if (message.error) {
pending.reject(new Error(message.error.message));
return;
}
pending.resolve(message.payload);
}
}
Использование:
const socket = new WebSocket("wss://example.com/ws");
socket.addEventListener("open", async () => {
const client = new WebSocketRpcClient(socket);
const profile = await client.request("get_profile", {
userId: 42,
});
console.log(profile);
});
Серверный ответ
Сервер должен вернуть тот же requestId:
socket.on("message", async (raw) => {
const message = JSON.parse(raw.toString());
if (message.type === "get_profile") {
socket.send(JSON.stringify({
type: "get_profile_response",
requestId: message.requestId,
payload: {
id: message.payload.userId,
name: "Dinar",
},
}));
}
});
Если сервер не возвращает requestId, клиент не сможет понять, какой Promise нужно завершить
Почему очередь нужна
Представьте, что клиент отправил три запроса:
get_profile requestId=a
get_orders requestId=b
get_stats requestId=c
Ответы могут прийти так:
get_stats requestId=c
get_profile requestId=a
get_orders requestId=b
Если сопоставлять ответы только по порядку, все сломается. requestId решает эту проблему
Обработка событий без ответа
Не все сообщения являются ответами. Некоторые события сервер присылает сам:
{
"type": "notification",
"payload": {
"text": "Новый заказ"
}
}
Для таких сообщений нет requestId. Их нужно обрабатывать отдельно:
if (!message.requestId) {
handleServerEvent(message);
return;
}
Что делать с повторным ответом
Иногда из-за повторной отправки, reconnect-логики или ошибки сервера ответ может прийти два раза. В этом случае клиент не должен повторно завершать Promise. Именно поэтому после первого ответа мы делаем:
this.pending.delete(message.requestId);
Если второй ответ придет позже, pending уже не найдется, и сообщение можно проигнорировать или залогировать как поздний дубль. Это нормальное защитное поведение для очереди ответов
Если операции критичные, добавьте на сервере идемпотентность: один requestId должен приводить к одному логическому результату, даже если сообщение было отправлено повторно
Частые ошибки
Первая ошибка — ждать return прямо из socket.send(). Метод send() только отправляет данные, он не возвращает ответ сервера
Вторая ошибка — хранить один общий callback. Если одновременно уйдет два запроса, ответы перепутаются
Третья ошибка — не ставить timeout. Если сервер не ответил, Promise будет висеть вечно
Четвертая ошибка — не удалять pending-запрос из Map. Это приводит к утечке памяти
Пятая ошибка — смешивать ответы и broadcast-события в одном обработчике без признака сообщения. У ответа должен быть requestId, у события может быть только type и payload
Самопроверка
Сделайте сервер, который отвечает на три запроса с разными задержками. Клиент должен правильно сопоставить ответы по requestId, даже если они пришли не по порядку. Потом отключите один ответ и проверьте, что timeout сработал и pending-запись удалилась
Что почитать дальше по WebSocket
Если нужен общий маршрут по теме, откройте рубрику WebSocket. Для соседних задач пригодятся эти разборы:
- Django: как подружить Celery с WebSocket
- Django: как сохранить биржевые данные, полученные по WebSocket
- FastAPI WebSocket: уведомления в браузер
- Fedora 43: как установить WebSocket сервер



