Skip to content

Recover from an in-dialog INVITE that arrives before our ACK - #1

Open
23Skidoo wants to merge 2 commits into
yay-com:mainfrom
23Skidoo:glare-fix
Open

23Skidoo wants to merge 2 commits into
yay-com:mainfrom
23Skidoo:glare-fix

Conversation

@23Skidoo

@23Skidoo 23Skidoo commented Aug 25, 2026 •

Copy link
Copy Markdown

https://app.clickup.com/t/2167239/869epm91r

An in-dialog INVITE that arrives while a session is still establishing — after the 200 OK, before we have sent our ACK — is dropped, and the server transaction it opened is left standing. That one transaction then blocks the dialog in both directions for the rest of the call: no re-INVITE of ours can be sent, so the session cannot be held or transferred, and every in-dialog INVITE arriving afterwards is answered 500 by the core without ever reaching the application.

The desktop app hits this on a call picked up with *35, where the platform refreshes the connected identity 28ms after the 200 OK and 36ms before the app's ACK. A user sees a call that cannot be held, and pressing Transfer leaves the far end listening to the whole consultation.

Two changes

Answer the request instead of dropping it. 491 Request Pending is what RFC 3261 s14.2 names for a request arriving while an offer/answer exchange is outstanding, and it invites the sender to retry once we settle. Any final status would free the transaction; this one also keeps the exchange recoverable. Rejecting also surfaces the request to delegate.onInvite, so an application can still see the identity the refresh carried.

Implement rollbackDescription on the web session description handler. Session.rollbackOffer is already called whenever an offer/answer exchange fails — on a non-2xx to our own re-INVITE, and when we cannot answer one we received — so that the next exchange starts from a stable peer connection. It resolves without doing anything when the handler implements no rollbackDescription, and the web one did not, so an offer applied by a failed exchange stayed applied and every later attempt failed while creating its description, on a session otherwise still up.

Testing

Four specs added to the existing web handler suite, covering a stable connection, an offer we created, an offer we received, and that a description can be built again afterwards. Verified against a real RTCPeerConnection in headless Chrome: removing the rollback call makes that last one fail with Invalid signaling state have-local-offer, the error from the original report.

The glare itself is covered outside this repo, driving this stack against a peer that sends its INVITE inside the window on purpose — red before these changes, green after, including on the app's own hold path.

Note on the suite. Three specs under API Session … Bob accept(), 200 has no SDP - Invalid 200 already fail on main, before these changes and after them. Nothing here touches that path.

They are caused by the Allow Invitation ACK without offer patch. Upstream treats a 2xx carrying no SDP, in answer to an offerless INVITE, as fatal — ACK, BYE, 481, terminate — and those specs assert exactly that. The patch's inviteWithoutSdp branch acknowledges and establishes instead, and the specs were never updated to match. Verified by bisection: pristine 0.21.2 is green, main fails those three on every run, and removing only that branch makes the suite green again.

Worth reconciling separately — either update the specs to the intended behaviour or gate the branch — because a permanently red baseline hides the next real regression.

Proposed upstream

This is an upstream bug, so the same two changes are proposed to onsip/SIP.js as onsip#1129. The fork ships it without waiting on that review; if upstream lands it first, this becomes a cherry-pick.

Dropping the request leaves the server transaction it opened standing, and
that one transaction then blocks the dialog in both directions for the rest
of the call: no re-INVITE of ours can be sent, so the session cannot be held
or transferred, and every in-dialog INVITE arriving afterwards is answered
500 by the core without ever reaching the application.

491 is the response RFC 3261 s14.2 names for a request that arrives while an
offer/answer exchange is outstanding, and it invites the sender to retry once
we settle. Any final status would free the transaction; this one also keeps
the exchange recoverable.
Session.rollbackOffer is called whenever an offer/answer exchange fails - on a
non-2xx response to our own re-INVITE, and when we cannot answer one we
received - so that the next exchange starts from a stable peer connection. It
resolves without doing anything when the handler implements no
rollbackDescription, and the web one did not, so an offer applied by a failed
exchange stayed applied and every later attempt failed while creating its
description, on a session that was otherwise still up.

A stable connection has nothing to discard, so that resolves rather than
raising; the pranswer states and a closed connection are refused the way the
neighbouring methods refuse them.
@23Skidoo
23Skidoo marked this pull request as ready for review October 20, 2026 08:42
@timwhite-nebula

Copy link
Copy Markdown

@claude

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants