diff --git a/packages/transport-webrtc/src/rtcpeerconnection-to-conn.ts b/packages/transport-webrtc/src/rtcpeerconnection-to-conn.ts index 07c7765093..dc3b39c6b1 100644 --- a/packages/transport-webrtc/src/rtcpeerconnection-to-conn.ts +++ b/packages/transport-webrtc/src/rtcpeerconnection-to-conn.ts @@ -29,6 +29,18 @@ class RTCPeerConnectionMultiaddrConnection extends AbstractMultiaddrConnection { this.peerConnection.close() } } + + // Handle the case where the peerConnection already reached a terminal state + // before this handler was registered (e.g. ICE failed during SDP exchange). + // Since onconnectionstatechange is a property assignment it won't fire for + // past state transitions, so we need to check the current state explicitly. + // Note: 'disconnected' is transient and may recover to 'connected', so only + // 'failed' and 'closed' are treated as terminal states here. + if (initialState === 'failed' || initialState === 'closed') { + this.log.trace('peer connection already in terminal state %s at construction time', initialState) + this.onTransportClosed() + this.peerConnection.close() + } } sendData (data: Uint8ArrayList): SendResult { diff --git a/packages/transport-webrtc/test/maconn.spec.ts b/packages/transport-webrtc/test/maconn.spec.ts index 6905f5ca47..78dc10e803 100644 --- a/packages/transport-webrtc/test/maconn.spec.ts +++ b/packages/transport-webrtc/test/maconn.spec.ts @@ -3,6 +3,7 @@ import { defaultLogger } from '@libp2p/logger' import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' +import delay from 'delay' import { stubObject } from 'sinon-ts' import { toMultiaddrConnection } from '../src/rtcpeerconnection-to-conn.ts' import { RTCPeerConnection } from '../src/webrtc/index.js' @@ -33,4 +34,54 @@ describe('Multiaddr Connection', () => { expect(maConn.timeline.close).to.not.be.undefined expect(metrics.increment.calledWith({ close: true })).to.be.true }) + + it('closes immediately when peer connection is already in failed state at construction time', async () => { + const peerConnection = { + connectionState: 'failed' as RTCPeerConnectionState, + onconnectionstatechange: null as any, + close: () => {} + } + + const remoteAddr = multiaddr('/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ') + + const maConn = toMultiaddrConnection({ + // @ts-expect-error - intentional mock + peerConnection, + remoteAddr, + direction: 'outbound', + log: defaultLogger().forComponent('libp2p:webrtc:connection') + }) + + // Give any microtasks or synchronous operations a chance to complete + await delay(0) + + expect(maConn.timeline.close).to.not.be.undefined + }) + + it('closes when peer connection transitions to failed state after construction', async () => { + const peerConnection: any = { + connectionState: 'connected' as RTCPeerConnectionState, + onconnectionstatechange: null as any, + close: () => {} + } + + const remoteAddr = multiaddr('/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ') + + const maConn = toMultiaddrConnection({ + peerConnection, + remoteAddr, + direction: 'outbound', + log: defaultLogger().forComponent('libp2p:webrtc:connection') + }) + + expect(maConn.timeline.close).to.be.undefined + + // Simulate peerConnection going to 'failed' after construction + peerConnection.connectionState = 'failed' + peerConnection.onconnectionstatechange?.() + + await delay(0) + + expect(maConn.timeline.close).to.not.be.undefined + }) })