openvibers/openvibe.chat Commits

A chat line that could not be saved is not broadcast: the sender gets an error and nobody sees a message that history, moderation and replays never have (the insert failure used to be swallowed as non-critical). ws-protocol test

OP OpenVibers committed 24 Sept 2026 · d268cf88d7 · 2 files, +19 −1


Changes

ddiff --git a/server/chat/chat-server.js b/server/chat/chat-server.js
iindex c73835c..824f007 100644
---- a/server/chat/chat-server.js
++++ b/server/chat/chat-server.js
@@@ -809,7 +809,13 @@ class ChatServer {
metadata: chatMsg.vip_badge ? { vip_badge: chatMsg.vip_badge } : undefined,
});
if (result.lastInsertRowid) chatMsg.id = Number(result.lastInsertRowid);
-- } catch { /* non-critical */ }
++ } catch (err) {
++ // Not saved means not sent: a line everyone saw but history, moderation and replays never
++ // have would be a ghost. Tell the sender and stop here.
++ console.warn('[Chat] message not saved, not broadcast:', err && err.message);
++ this.sendTo(ws, { type: 'error', message: 'Your message could not be sent. Please try again.' });
++ return;
++ }
// Attach reply context to broadcast
if (replyTo) chatMsg.reply_to = replyTo;
ddiff --git a/test/ws-protocol.test.js b/test/ws-protocol.test.js
iindex 92d302b..7a31139 100644
---- a/test/ws-protocol.test.js
++++ b/test/ws-protocol.test.js
@@@ -108,6 +108,18 @@ t('chat into a stream room: room delivery, global cross-feed, persistence, outbo
assert.strictEqual(reaction.body.powerchat_chat.externalChatterId, `u${alice.id}`);
});
++t('a message that could not be saved is not broadcast; the sender is told', async () => {
++ const orig = h.db.saveChatMessage;
++ h.db.saveChatMessage = () => { throw new Error('disk full'); };
++ try {
++ bobWs.sendJson({ type: 'chat', message: 'ghost line' }); // bob has not spoken yet (no rate limit)
++ const err = await bobWs.next((m) => m.type === 'error' && /could not be sent/.test(m.message));
++ assert.ok(err);
++ await new Promise((r) => setTimeout(r, 200));
++ assert.ok(!aliceWs.all.some((m) => m.type === 'chat' && m.message === 'ghost line'), 'nobody else received it');
++ } finally { h.db.saveChatMessage = orig; }
++});
++
t('stream history and channel history return the message like Live did', async () => {
const hist = await h.http('GET', `/api/chat/${streamId}/history`);
assert.strictEqual(hist.status, 200);