Skip to content

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

Open
23Skidoo wants to merge 2 commits into
onsip:mainfrom
23Skidoo:upstream-glare-recovery
Open

Recover from an in-dialog INVITE that arrives before our ACK#1129
23Skidoo wants to merge 2 commits into
onsip:mainfrom
23Skidoo:upstream-glare-recovery

Conversation

@23Skidoo

@23Skidoo 23Skidoo commented Aug 26, 2026

Copy link
Copy Markdown

An in-dialog INVITE arriving after the 200 OK but before we send our ACK is dropped by Session.onInviteRequest, and the server transaction the core opened for it is left standing. That one transaction then blocks the dialog in both directions for the rest of the call: SessionDialog.invite throws "There is an ongoing re-INVITE server transaction." for every re-INVITE we try to send — so SimpleUser.hold() fails permanently — and the core statelessly answers every later in-dialog INVITE with 500 before the application sees it.

We hit this in production with a PBX that sends a connected-identity re-INVITE 28ms after its 200 OK, 36ms before the client's ACK: the user answers a call and can never hold or transfer it.

Two changes.

onInviteRequest now rejects the early INVITE with 491 Request Pending instead of dropping it — the response RFC 3261 section 14.2 names for a request arriving while an offer/answer exchange is outstanding. Rejecting completes the server transaction, which is what unblocks the dialog, and surfaces the request to delegate.onInvite.

The web SessionDescriptionHandler now implements rollbackDescription. Session.rollbackOffer already calls it whenever an offer/answer exchange fails — on a non-2xx answer to our own re-INVITE, and when we cannot answer one we received — but no handler implemented it, so those rollbacks were silent no-ops and an offer applied by a failed exchange stayed applied, leaving the peer connection unable to create its next description.

Four specs added for the rollback, verified against a real RTCPeerConnection: without the rollback call, retrying after a failed exchange fails with Invalid signaling state have-local-offer. The glare recovery is reproduced end to end outside this repo by driving the stack against a peer that deliberately sends its INVITE inside the 200-to-ACK window — red before these changes, green after.


This contribution was prepared with LLM assistance (Claude): the investigation, the change, and this description. A human reviewed it and stands behind the submission.

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.
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.

1 participant