Skip to content

fix: Failed to set remote offer sdp: Duplicate a=mid value 'datachannel' error#371

Closed
evgmel wants to merge 1 commit into
versatica:v3from
evgmel:fix-sdp-err
Closed

fix: Failed to set remote offer sdp: Duplicate a=mid value 'datachannel' error#371
evgmel wants to merge 1 commit into
versatica:v3from
evgmel:fix-sdp-err

Conversation

@evgmel
Copy link
Copy Markdown
Contributor

@evgmel evgmel commented Apr 29, 2026

This PR fixes Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote offer sdp: Duplicate a=mid value 'datachannel' error.

Summary

When receiveDataChannel() fails after receiveSctpAssociation() mutates the
internal RemoteSdp state but before _hasDataChannelMediaSection is set to
true, any subsequent receiveDataChannel() call adds a second
m=application section with mid=datachannel to the SDP. The browser then
rejects the offer with:

Failed to execute 'setRemoteDescription' on 'RTCPeerConnection':
Failed to set remote offer sdp: Duplicate a=mid value 'datachannel'

After this point the transport is permanently broken and cannot create any new
data channels.

Root cause

In every handler (Chrome111, Chrome74, Safari12, Firefox120,
ReactNative106) the receiveDataChannel() method follows this sequence:

if (!this._hasDataChannelMediaSection) {
    this._remoteSdp.receiveSctpAssociation();   // 1. mutates RemoteSdp
    const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() };
    await this._pc.setRemoteDescription(offer); // 2. can throw
    // ...
    this._hasDataChannelMediaSection = true;    // 3. never reached on failure
}

receiveSctpAssociation() unconditionally pushes a new OfferMediaSection
with mid: 'datachannel' into _mediaSections and _midToIndex. If step 2
throws, the section remains in the SDP but the guard flag stays false. The
next call enters the if block again, calls receiveSctpAssociation() again,
and produces a duplicate a=mid.

The same pattern exists in sendSctpAssociation() for the send direction.

The fix in the PR

The fix ensures that even if _hasDataChannelMediaSection is out of sync with the
actual SDP state, the media section is never duplicated (idempotent).

@evgmel
Copy link
Copy Markdown
Contributor Author

evgmel commented Apr 29, 2026

Steps to reproduce

1. Setup

Any application that creates multiple data consumers on a single receive
transport via transport.consumeData() can trigger this. The simplest case is
two sequential consumeData() calls where the first one fails at the WebRTC
level.

2. Inject a one-shot failure

In src/handlers/Chrome111.ts (or the compiled .js), add a simulated
failure inside receiveDataChannel() right after receiveSctpAssociation():

if (!this._hasDataChannelMediaSection) {
    this._remoteSdp.receiveSctpAssociation();

    // --- temporary: simulate first-call failure ---
    if (!(this as any).__debugFirstDataChannelFailed) {
        (this as any).__debugFirstDataChannelFailed = true;
        throw new Error('Simulated first receiveDataChannel failure');
    }
    // --- end temporary ---

    const offer = { type: 'offer' as RTCSdpType, sdp: this._remoteSdp.getSdp() };
    await this._pc.setRemoteDescription(offer);
    // ...
}

3. Trigger the bug

Call transport.consumeData() twice on the same receive transport (the calls
can target different data producers):

// First call — hits the injected throw after receiveSctpAssociation().
// The caller catches/swallows the error.
try {
    await recvTransport.consumeData(optionsA);
} catch {
    // error swallowed — transport SDP is now corrupted
}

// Second call — receiveSctpAssociation() is called again because
// _hasDataChannelMediaSection is still false, producing a duplicate
// a=mid:'datachannel' in the SDP.
await recvTransport.consumeData(optionsB);
// ^ throws: "Duplicate a=mid value 'datachannel'"

4. Observe the error

The second consumeData() throws:

DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection':
Failed to set remote offer sdp: Duplicate a=mid value 'datachannel'

@evgmel
Copy link
Copy Markdown
Contributor Author

evgmel commented Apr 29, 2026

Hello @ibc ! Could you please take a look at the PR?

@ibc
Copy link
Copy Markdown
Member

ibc commented Apr 29, 2026

This solution is not valid because it fails if transport.consumeData() is called twice with same options.id and the first call fails. The problem is that in both cases this code is executed:

const options = {
			negotiated: true,
			id: streamId,
			ordered,
			maxPacketLifeTime,
			maxRetransmits,
			protocol,
		};

const dataChannel = this._pc.createDataChannel(label!, options);

@evgmel
Copy link
Copy Markdown
Contributor Author

evgmel commented Apr 29, 2026

@ibc In our app we create 2 different data consumers for different data producers on a receive transport. I've checked - options.id is different when I tried to emulate the error on first call. The consumers created one after another (not in parallel), but the creation of the first one was wrapped in try/catch in our code, as data from that consumer was supposed not critical for the app.
Are you sure that options.id can be the reason here? We can check something else on our side if necessary.

@ibc
Copy link
Copy Markdown
Member

ibc commented Apr 29, 2026

I'm sorry but I will close this PR. Rationale is the following:

mediasoup-client was absolutely never designed to be resilient to errors in PeerConnection calls. Those errors should not happen in a properly configured application. There is lot of internal state within mediasoup-client handlers (Chrome111, etc) that get into inconsistent state if something fails (such as PeerConnection calls or events that require sending a message from the client app to the server and obtaining a response from it, such as "connect", "produce" or "produceData" events of mediasoup-client.Transport class). mediasoup-client also requires that the signaling protocol that the client app uses to send and receive messages is reliable. And if anything fails then the client app should close everything and restart/reconnect the whole client app. There is no rollback mechanisms in mediasoup-client.

This PR covers a single use case (transport.consumeData(streamId 1) fails and then transport.consumeData(streamId 2) does not fail), but it doesn't cover the case in which consumeData() is called with same streamId. And even if some workaround could be done to solve this specific scenario, there are still tons of code and logic that get into an inconsistent state if any PeerConnection method fails.

If pc.setRemoteDescription() fails in your application then there is something wrong at your application level because it just shouldn't happen. Both mediasoup-client and mediasoup guarantee that they produce ICE/RTP/DTLS/SCTP parameters than can be safely converted into valid SDPs that the browser will accept.

@ibc ibc closed this Apr 29, 2026
@evgmel evgmel deleted the fix-sdp-err branch April 30, 2026 04:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants