Skip to content

dialog_client: preserve routing and isolate retransmitted ACKs - #369

Merged
emiago merged 3 commits into
emiago:mainfrom
RetellAI:fix/dialog-ack-retransmission-race
Oct 20, 2026
Merged

emiago merged 3 commits into
emiago:mainfrom
RetellAI:fix/dialog-ack-retransmission-race

Conversation

@ddas-rt

@ddas-rt ddas-rt commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #368.

Build ACK routing and headers before taking an immutable snapshot, then send a fresh copy per retransmission without rebuilding dialog headers. This prevents the shared-request race and retries bypassing the Record-Route proxy.

Removed the added tests at maintainer request. Existing comments and tests are preserved; existing dialog-client tests pass under -race (3 runs).

…nic)

WriteAck cloned the ACK once and re-sent that same *sip.Request on every 200 OK
retransmission. Each resend runs WriteRequest -> ClientRequestAddVia ->
Request.PrependHeader, which mutates the request's header slice in place. Each
retransmitted 2xx is delivered on its own transaction-layer goroutine (passUpRetransmission
releases the FSM lock before invoking the hook), so two close retransmissions race on
the one shared request and panic in PrependHeader:

    panic: runtime error: index out of range [N] with length N
      sip.(*headers).PrependHeader
      ClientRequestAddVia
      DialogClientSession.WriteAck.func1
      ClientTx.passUpRetransmission

The panic is raised in the transaction-layer goroutine, so callers cannot recover it
and the process crashes. Reusing the clone also appends a duplicate Via on every resend.

Fix: snapshot the ACK once and send a fresh clone per retransmission. Concurrent
Clone() calls are concurrent reads of the immutable snapshot (safe); each resend
mutates its own copy.

Adds TestDialogClientACKRetransmissionOverlap, which overlaps the retransmission
callbacks and asserts one Via per ACK. It fails under -race on the shared request and
passes with the fix; the existing sequential test does not catch it.
@ddas-rt

ddas-rt commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

@emiago Bumping this up.

@ddas-rt

ddas-rt commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Updated in 9d5f4da: snapshot the fully routed ACK and replay fresh copies without rebuilding dialog headers. Added routing and real TCP/authentication regressions; focused race tests pass. This also fixes retransmitted ACKs bypassing a Record-Route proxy because Clone cached the Contact destination too early.

@ddas-rt ddas-rt changed the title dialog_client: clone ACK per 2xx retransmission (fix PrependHeader panic) dialog_client: preserve routing and isolate retransmitted ACKs Sep 30, 2026
@emiago

emiago commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Tests are very here complex, or testing maybe more like some integration. Can you remove them for now. I do not want too much AI slop ending up.

@ddas-rt

ddas-rt commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

@emiago Removed the added tests in aa3557f. The PR now only changes dialog_client.go; the ACK fix and existing comments are unchanged. Existing dialog-client tests pass under -race (3 runs).

Comment thread dialog_client.go
}

if err := s.WriteRequest(retransmissionAck); err != nil {
if err := s.UA.Client.WriteRequest(ackTemplate.Clone(), s.requestValidate); err != nil {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is point of second Clone? After this request is serilaized

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second clone gives each retransmission its own request. Before serialization, ClientRequestConnection writes req.raddr and can update the Via host/port.

Retransmission callbacks can overlap because passUpRetransmission releases fsmMu before calling the hook. Reusing ackTemplate would let those writes race with another callback’s transport setup or serialization. The first clone isolates the template from the initial send; the second keeps the template immutable across retransmissions.

This is a defensive strategy to handle the case where UA receives retransmitted ACKs in quick succession, that is addressed to a different endpoint and it needs to forward it to the next destination.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then problem is overlapping, if I see, but I could only right now think more complex solution.

@emiago

emiago commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Thanks for detailed explanation. Was busy with other stuff. merging

@emiago
emiago merged commit bddd307 into emiago:main Oct 20, 2026
1 check passed
@ddas-rt

ddas-rt commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Awesome! Thanks @emiago .

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.

panic: index out of range in headers.PrependHeader on 2xx-retransmission ACK resend (DialogClientSession.WriteAck)

2 participants