Repository navigation
fix(transaction): keep a server INVITE's final response after the ACK ends it - #174
Merged
shenjinti merged 1 commit intoOct 7, 2026
Conversation
… ends it Since restsend#169 the matching ACK terminates an Accepted server INVITE transaction, and cleanup() took last_response before the dialog built DialogState::Confirmed from it. Confirmed carried Response::default() (no CSeq) for the initial INVITE and every re-INVITE, so a TU could not tell which INVITE it confirms. The server INVITE transaction now keeps last_response and hands a copy to finished_transactions.
This was referenced Oct 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #173.
Problem
Since #169 the matching ACK terminates an Accepted server INVITE transaction (
transaction.rs:918) before the ACK is returned to the dialog.cleanup()takeslast_response(transaction.rs:1615-1617), so the dialog'sConfirmed(id, tx.last_response.clone().unwrap_or_default())(server_dialog.rs:865,:905;invite_dialog.rs:863,:903) carriesResponse::default(): no CSeq or any other header, for the initial INVITE and every re-INVITE.Spec
DialogStatedocs:Confirmedmeans "2xx response received/sent and ACK sent/received", and the variant carries that 2xx. It did on the feat(transaction): RFC 6026 Accepted state + Timer L/M (adopted from #128) #164 merge (f27b7c8) and stopped on the fix(transaction): end the server Accepted state on the matching ACK (review follow-up to #164) #169 merge (52ac300).Confirmedto its INVITE, for example when re-INVITEs overlap.Fix
In
Transaction::cleanup()a server INVITE transaction now givesfinished_transactionsa clone oflast_responseinstead of taking it. Server non-INVITE transactions still move theirs. Nothing in the dialog layer changes.Side effects, checked:
cleanup()still cancels Timer G and Timer L first (cleanup_timer()), and the transaction is detached from the endpoint.on_timerdoes nothing inTerminated, andrespond()cannot leaveTerminated(can_transition). A retransmitted INVITE or ACK is still absorbed byfinished_transactions, which gets the same response as before.last_responseof a terminated server INVITE to decide anything: the only readers after the ACK are theConfirmedbuilders above. The Timer L path (no ACK) never buildsConfirmed; it goes toend_session_without_ack, whoseanswered_2xxis computed before the receive loop.Response(with its body) until its owner drops it, which for the dialog layer is the end ofhandle_invite/handle_reinvite, right afterConfirmed. Before fix(transaction): end the server Accepted state on the matching ACK (review follow-up to #164) #169 it was kept the same way (the transaction stayed alive in Completed/Accepted with its response). No cycle, no growth with the number of calls.InviteDialogandClientInviteDialogbuildConfirmedfrom the 2xx returned byreceive(), not from the terminated transaction.Contract / coverage
DialogState::Confirmedcarries the 2xx its ACK confirms: status 200 andCSeq: N INVITEof that INVITE. This holds for the initial INVITE and for each re-INVITE, on every transport (the transaction path is transport independent; tests cover UDP and TCP).Confirmedre-published after an in-dialog non-INVITE (return_to_confirmedafter INFO, UPDATE, MESSAGE, ...) still carries an empty response, because a server non-INVITE transaction terminates on its final response and still moves it. That is not related to fix(transaction): end the server Accepted state on the matching ACK (review follow-up to #164) #169, and the INFO's 200 would not be the response that confirmed the dialog anyway, so it is left alone here.Tests
src/dialog/tests/test_uas_ack_timeout.rs(the raw-peer UAS harness):wait_confirmednow returns the CSeq number of the response inConfirmed, after asserting it is a 200 with a CSeq whose method is INVITE.test_2xx_over_tcp_is_retransmitted_until_the_ack: initial INVITE over TCP,Confirmedcarries CSeq 1.test_late_ack_of_an_earlier_reinvite_reaches_its_own_transaction: initial INVITE over UDP carries CSeq 1; then ACK 2 (late, after re-INVITE 3 was answered) and ACK 3 produceConfirmedwith CSeq 2 and then CSeq 3, in that order.On
main(5ef8ea6) both fail:With the initial-INVITE assertion of the second test disabled, the re-INVITE
Confirmedfails the same way onmain. The TCP assertion passes on f27b7c8 (#164) and fails on 52ac300 (#169).Checks
cargo test --features bench: 385 lib tests passed, 0 failed (as onmain; existing tests were extended, none added), and 65 doc tests passed. Plaincargo testpasses too.cargo check --no-default-features --features platform-embassy: no warnings, as onmain.cargo clippy --features bench --all-targets: the same output as onmain, none in the changed code. (Onmainit stops at aclippy::never_looperror insrc/dialog/tests/test_refer_notify.rs:98, unrelated to this PR.)rustfmt --checkon the changed files: clean.