From bd6efaaa4e47875dbd678355b21ba36fbf8d4c3c Mon Sep 17 00:00:00 2001 From: Felix-Ayush <67006255+Ayush7614@users.noreply.github.com> Date: Thu, 30 Jul 2026 04:28:39 +0530 Subject: [PATCH] fix(puter-js): ignore peer offers for unknown connections (#3473) createAnswer ran outside the connection null-check, so stale or unknown offer ids threw on the signalling socket. Guard the whole offer path and return the #message promise from onmessage so callers can observe errors. --- src/puter-js/src/modules/Peer.js | 14 ++-- src/puter-js/src/modules/Peer.server.test.js | 82 ++++++++++++++++++++ 2 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 src/puter-js/src/modules/Peer.server.test.js diff --git a/src/puter-js/src/modules/Peer.js b/src/puter-js/src/modules/Peer.js index 7832a6066..e223cfa97 100644 --- a/src/puter-js/src/modules/Peer.js +++ b/src/puter-js/src/modules/Peer.js @@ -44,7 +44,7 @@ class PuterPeerConnectionErrorEvent extends Event { } } -class PuterPeerServer extends EventTarget { +export class PuterPeerServer extends EventTarget { #wsconn; #oncreateresolve; @@ -83,7 +83,7 @@ class PuterPeerServer extends EventTarget { this.#wsconn.onmessage = (event) => { let data = JSON.parse(event.data); - this.#message(data); + return this.#message(data); }; this.#wsconn.onclose = () => { @@ -168,12 +168,12 @@ class PuterPeerServer extends EventTarget { if ( data.server.offer ) { let uuid = data.server.offer.id; let connection = this.connections.get(uuid); - if ( connection ) { - await connection.setRemoteDescription( - new RTCSessionDescription(data.server.offer.offer), - ); + if ( ! connection ) { + return; } - + await connection.setRemoteDescription( + new RTCSessionDescription(data.server.offer.offer), + ); const answer = await connection.createAnswer(); this.#wsconn.send( JSON.stringify({ diff --git a/src/puter-js/src/modules/Peer.server.test.js b/src/puter-js/src/modules/Peer.server.test.js new file mode 100644 index 000000000..fbc4bdfeb --- /dev/null +++ b/src/puter-js/src/modules/Peer.server.test.js @@ -0,0 +1,82 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { PuterPeerServer } from './Peer.js'; + +/** + * Orphan offers (unknown connection id) used to call createAnswer on + * undefined after the setRemoteDescription guard, producing an unhandled + * rejection on the signalling websocket. + */ + +class FakeWebSocket { + static latest = null; + sent = []; + onopen = null; + onmessage = null; + onerror = null; + onclose = null; + + constructor () { + FakeWebSocket.latest = this; + } + + send (data) { + this.sent.push(data); + } + + close () {} +} + +const origWebSocket = globalThis.WebSocket; + +beforeEach(() => { + FakeWebSocket.latest = null; + globalThis.WebSocket = FakeWebSocket; +}); + +afterEach(() => { + globalThis.WebSocket = origWebSocket; +}); + +const flushMicrotasks = async () => { + await Promise.resolve(); + await Promise.resolve(); +}; + +const startServer = async () => { + const server = new PuterPeerServer({ + signallerUrl: 'ws://signaller.test/', + authToken: 'token', + }); + const started = server.start(); + // Resolve the open handshake, then let start() install onmessage. + FakeWebSocket.latest.onopen(); + await flushMicrotasks(); + await FakeWebSocket.latest.onmessage({ + data: JSON.stringify({ + server: { create: { success: true, invitecode: 'invite-1' } }, + }), + }); + await started; + return server; +}; + +describe('PuterPeerServer orphan offers', () => { + it('ignores offers for unknown connection ids without throwing', async () => { + await startServer(); + const ws = FakeWebSocket.latest; + const sentBefore = ws.sent.length; + + await expect(ws.onmessage({ + data: JSON.stringify({ + server: { + offer: { + id: 'missing-connection', + offer: { type: 'offer', sdp: 'v=0' }, + }, + }, + }), + })).resolves.toBeUndefined(); + + expect(ws.sent.length).toBe(sentBefore); + }); +});