Skip to content

fix(dialog): ACK and CANCEL keep the CSeq of the request they refer to - #355

Open
alexvrf wants to merge 1 commit into
emiago:mainfrom
alexvrf:fix/dialog-ack-cseq
Open

alexvrf wants to merge 1 commit into
emiago:mainfrom
alexvrf:fix/dialog-ack-cseq

Conversation

@alexvrf

@alexvrf alexvrf commented Sep 6, 2026

Copy link
Copy Markdown

DialogClientSession.buildReq derives the CSeq of every request within the dialog from the dialog counter, ACK and CANCEL included. RFC 3261 requires the opposite: an ACK carries the sequence number of the INVITE it acknowledges (13.2.2.4) and a CANCEL the one of the request being cancelled (9.1), and neither consumes a number of its own.

The two values differ as soon as anything is sent between the INVITE and its final response. With reliable provisional responses that is the normal case: every PRACK moves the counter on, so the ACK for the 2xx leaves with the sequence number of the last PRACK. The UAS does not match it to the INVITE, keeps retransmitting the 2xx until the timer expires and then releases the call, which makes every call using 100rel fail to establish.

Take the number from the CSeq the caller set, or from the initial INVITE filled in above, and leave the dialog counter alone so that the next request stays above the PRACK instead of colliding with it. An ACK for a re-INVITE keeps working: the caller sets the CSeq of that re-INVITE and it is no longer overwritten.

The added tests fail on the code before this change and pass after it.

@emiago

emiago commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Thanks, the ACK/PRACK fix makes sense. One issue remains: this regresses ACKs for re-INVITEs when the caller does
not explicitly set a CSeq.

buildReq initializes a missing CSeq from the initial INVITE, and ACK now skips the dialog-counter assignment. After
a re-INVITE with CSeq N+1, calling WriteAck with a fresh ACK therefore sends CSeq N.

Previously this inherited the current dialog CSeq, and WriteAck does not document that callers must set it manually.
Could you preserve that behavior or make the API explicitly associate the ACK with the INVITE it acknowledges?
Please also add a test for a re-INVITE ACK without a pre-set CSeq.

@alexvrf
alexvrf force-pushed the fix/dialog-ack-cseq branch from 189f966 to b9e6faf Compare September 7, 2026 07:46
DialogClientSession.buildReq derives the CSeq of every request within the
dialog from the dialog counter, ACK and CANCEL included. RFC 3261 requires
the opposite: an ACK carries the sequence number of the INVITE it
acknowledges (13.2.2.4) and a CANCEL the one of the request being
cancelled (9.1), and neither consumes a number of its own.

The two values differ as soon as anything is sent between the INVITE and
its final response. With reliable provisional responses that is the normal
case: every PRACK moves the counter on, so the ACK for the 2xx leaves with
the sequence number of the last PRACK. The UAS does not match it to the
INVITE, keeps retransmitting the 2xx until the timer expires and then
releases the call, which makes every call using 100rel fail to establish.

The number an ACK or a CANCEL carries is therefore, in order: the one the
caller set on the request, or the one of the last INVITE sent within the
dialog. The dialog keeps that number in lastInviteCSeqNo, next to the
counter it already had. Deriving it from the counter is what breaks with
100rel, and taking it from Dialog.InviteRequest alone would break the ACK
for a re-INVITE built without a CSeq header, which WriteAck accepts and
does not ask callers to fill in.

The counter itself is left alone by ACK and CANCEL, so that the next
request stays above the PRACK instead of colliding with it.

The added tests fail on the code before this change and pass after it:
the ACK after two PRACKs, an ACK whose CSeq the caller set, and an ACK for
a re-INVITE built without one.
@alexvrf
alexvrf force-pushed the fix/dialog-ack-cseq branch from b9e6faf to 1b77f01 Compare September 7, 2026 08:21
@alexvrf

alexvrf commented Sep 7, 2026

Copy link
Copy Markdown
Author

Good catch, thanks — the re-INVITE ACK without an explicit CSeq did regress, and I've reproduced it in a test.

Restoring the old behaviour would bring back the original bug, though: the dialog counter is wrong whenever a PRACK is sent between the INVITE and its final response, and that applies to a re-INVITE using 100rel just as much as to the initial one — there both the counter and Dialog.InviteRequest give the wrong number.

So the number an ACK or CANCEL carries is now, in order: the one the caller set on the request, otherwise the one of the last INVITE sent within the dialog, which the dialog keeps in lastInviteCSeqNo next to the counter it already had. WriteAck still accepts a bare ACK; the default is simply correct now.

Added TestDialogClientAckCSeqReInviteWithoutHeader for exactly the case you described — it fails without lastInviteCSeqNo, with the sequence number of the initial INVITE.

The branch is rebased onto current main (dfd6dc5) and go test ./... passes.

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