Skip to content

fix(transaction): pass a reliable provisional with a new RSeq to the TU - #195

Merged
yeoleobun merged 1 commit into
restsend:mainfrom
tgeorge06:fix/reliable-provisional-new-rseq
Oct 10, 2026
Merged

yeoleobun merged 1 commit into
restsend:mainfrom
tgeorge06:fix/reliable-provisional-new-rseq

Conversation

@tgeorge06

Copy link
Copy Markdown
Contributor

Fixes #194.

Thanks to @nbasil for the report. The analysis and the proposed fix in the issue are exact; this PR implements them with the existing typed rseq_value() accessor and adds the tests the issue describes.

Problem

Transaction::on_received_response drops a response as a retransmission when the transaction stays in the same state and the previous response has the same status code and body (src/transaction/transaction.rs:1028 at current main, 1f15ba9). #79 made a second 183 with a different body reach the TU, but a second reliable 183 (RFC 3262) with a new RSeq and the same SDP still matches and is dropped. The dialog never sends its PRACK, the UAS retransmits the 183 for 64*T1 and then fails the INVITE with a 5xx.

Expected

RFC 3261 §17.1.1.2:

Any further provisional responses MUST be passed up to the TU while in the "Proceeding" state.

RFC 3262 §3:

The value of the RSeq in each subsequent reliable provisional response for the same request MUST be greater by exactly one.

RFC 3262 §4:

Once a reliable provisional response is received, retransmissions of that response MUST be discarded. A response is a retransmission when its dialog ID, CSeq, and RSeq match the original response.

So two reliable 183s that differ only in RSeq are two responses, and both must reach the TU.

Fix

The duplicate check also compares RSeq, through the existing HeadersExt::rseq_value() (src/sip/message.rs:328, the parsed u32 the dialog's PRACK code already uses). +7 / -1 lines in src/transaction/transaction.rs; the rest of the +128 / -2 is tests.

Unchanged on purpose:

  • A response without RSeq gives None on both sides, so unreliable provisionals, retransmitted 3xx-6xx in Completed and non-INVITE client responses are deduplicated exactly as before.

  • An exact retransmission of a reliable provisional keeps its RSeq and is still dropped.

  • The Accepted exemption for 2xx (RFC 6026) is untouched.

  • What to do with the RSeq stays with the dialog: prepare_prack_request (src/dialog/dialog.rs:606) PRACKs a higher RSeq, re-sends the cached PRACK for the same one and ignores a lower one (lines 626-633), as before.

  • The dialog's own RFC 3262 §4 ordering rule is not changed here. §4 says:

    If the UAC receives another reliable provisional response to the same request, and its RSeq value is not one higher than the value of the sequence number, that response MUST NOT be acknowledged with a PRACK, and MUST NOT be processed further by the UAC.

    Today prepare_prack_request PRACKs any higher RSeq (also one that skips a value), and process_invite still moves to Early for a lower one without a PRACK (src/dialog/client_dialog.rs:755-756, src/dialog/invite_dialog.rs:292-293). With this PR a late copy of an older reliable 183 with the same body as the newer one also reaches the dialog (one more Early notification, no PRACK), as one with a different body already did. A UAS may not send the next reliable provisional before the previous one is PRACKed (§3), so that only happens when a retransmission of the previous one is still in flight. Tightening the dialog to "exactly one higher" is a separate change; happy to send it as a follow-up.

Contract / coverage

What the TU of a client INVITE transaction in Proceeding gets, when the new response has the same status and body as the last one:

previous new before after
no RSeq no RSeq dropped dropped
RSeq: n RSeq: n (retransmission) dropped dropped
RSeq: n RSeq: n+1 dropped passed to the TU
no RSeq RSeq: 1 dropped passed to the TU

A different status or body was already passed to the TU and still is.

Tests

  • src/transaction/tests/test_provisional_responses.rs, test_multiple_provisional_responses: after the existing 183s, a reliable 183 with the same body and RSeq: 1, then RSeq: 2, each sent twice. Each RSeq reaches the TU once; the copy is ignored. On main it fails with 183 with RSeq 1 must reach the TU.
  • src/dialog/tests/test_prack.rs, client_dialog_pracks_each_reliable_provisional: a client dialog (DialogLayer::do_invite with support_prack: true, so the INVITE carries Supported: 100rel) against a raw UDP peer that sends two reliable 183s with the same SDP and RSeq 1 and 2. The UAC must send a PRACK with RAck: 1 and then one with RAck: 2. On main it times out waiting for the second PRACK.

Checks

  • cargo test --features bench: 419 lib tests passed, 0 failed (418 on main plus the new one), and 65 doc tests passed. Plain cargo test passes too.
  • cargo check --no-default-features --features platform-embassy: no warnings, as on main.
  • cargo clippy --features bench --all-targets: the same output as on main, none in the changed code. (On main it stops at a clippy::never_loop error in src/dialog/tests/test_refer_notify.rs:98, unrelated to this PR; with -A clippy::never_loop the warnings are the same as on main.)
  • cargo fmt --check: clean.

A second reliable 183 (RFC 3262) with a new RSeq and the same body as
the previous one was dropped as a retransmission, so the TU never sent
its PRACK; the UAS retransmitted it for 64*T1 and then failed the
INVITE. The duplicate check now also compares RSeq. An exact
retransmission keeps its RSeq and is still ignored.

Fixes restsend#194.
@yeoleobun
yeoleobun merged commit 6fd9414 into restsend:main Oct 10, 2026
3 checks passed
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.

Client INVITE transaction drops a second reliable 183 (new RSeq, same body)

2 participants