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.
This commit is contained in:
Felix-Ayush
2026-07-29 15:58:39 -07:00
committed by GitHub
parent 2b6dc528ce
commit bd6efaaa4e
2 changed files with 89 additions and 7 deletions
+7 -7
View File
@@ -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({
@@ -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);
});
});