Skip to content

Fix 4006 reconnect#536

Closed
nicklipple wants to merge 2 commits intodisgoorg:masterfrom
nicklipple:fix-4006-reconnect
Closed

Fix 4006 reconnect#536
nicklipple wants to merge 2 commits intodisgoorg:masterfrom
nicklipple:fix-4006-reconnect

Conversation

@nicklipple
Copy link
Copy Markdown

@nicklipple nicklipple commented Apr 8, 2026

When a voice gateway drops and the reconnect's Resume is rejected with close code 4006 ("Session is no longer valid"), doReconnect treated it as non-reconnectable and gave up. This tore down the entire voice connection and the bot stopped receiving audio.

Gateway fix (voice/gateway.go): Exempt 4006 from the non-reconnectable bail-out in doReconnect. Since open() already clears resume state (ssrc/seq) on failure, the retry naturally sends Identify instead of Resume. Also made open() injectable (openFunc) for testability.

Audio pipeline fix (voice/conn.go, voice/udp_conn.go): The Identify path produces a new Ready → new UDP socket, but the audio sender/receiver goroutines were still reading from the old one. Two changes:

  • udpConnImpl.Open() now closes the previous net.Conn before dialing a new one (prevents socket leak, unblocks stale readers)
  • connImpl.handleMessage restarts the audio sender/receiver on SessionDescription if they were previously configured

@nicklipple nicklipple marked this pull request as ready for review April 8, 2026 18:37
@topi314 topi314 requested a review from Copilot April 8, 2026 18:37
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes voice reconnect behavior when Discord rejects Resume with close code 4006 by falling back to Identify, and ensures the audio pipeline restarts correctly when a new UDP socket/session is established after reconnect.

Changes:

  • Voice gateway reconnect: treat 4006 as retryable and inject open() via openFunc for testability.
  • UDP transport: close any prior UDP net.Conn before dialing a new one to prevent socket leaks and unblock stale readers.
  • Audio pipeline: restart sender/receiver on SessionDescription when previously configured; adds focused unit tests for both gateway and conn behaviors.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
voice/gateway.go Allows reconnect retry on 4006 and introduces injectable openFunc.
voice/gateway_test.go Adds tests verifying 4006 retry behavior and non-reconnectable close codes.
voice/udp_conn.go Closes previous UDP connection before re-dialing to avoid leaks/stale reads.
voice/conn.go Restarts audio sender/receiver on SessionDescription when configured.
voice/conn_test.go Adds tests validating audio pipeline restart behavior on SessionDescription.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread voice/conn.go
c.audioReceiver.Open()
}

c.openedChan <- struct{}{}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

handleMessage always sends to openedChan on every SessionDescription. Since openedChan is only read once by Conn.Open, subsequent reconnects (Identify path) will leave the channel full and the next c.openedChan <- struct{}{} will block the gateway message handler, potentially deadlocking voice reconnects. Consider making this send non-blocking (e.g. select { case c.openedChan <- struct{}{}: default: }) or otherwise draining/resetting the channel on reconnect.

Suggested change
c.openedChan <- struct{}{}
select {
case c.openedChan <- struct{}{}:
default:
}

Copilot uses AI. Check for mistakes.
Comment thread voice/udp_conn.go
Comment on lines +194 to +198
// Close any existing connection so that readers (e.g. audio receiver)
// get an error and can be restarted on the new connection.
if u.conn != nil {
_ = u.conn.Close()
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

After closing an existing u.conn during Open, consider also setting u.conn = nil and making UDPConn.Close()/deadline/addr methods nil-safe. If DialContext (or the IP discovery write/read) fails, callers may still invoke Close() (e.g. during teardown), and udpConnImpl.Close() currently dereferences u.conn unconditionally, which can panic when u.conn is nil.

Copilot uses AI. Check for mistakes.
@nicklipple
Copy link
Copy Markdown
Author

this PR isn't ready as pointed out by Copilot. Needs more testing around shared state in different goroutines

@nicklipple nicklipple closed this Apr 8, 2026
@topi314
Copy link
Copy Markdown
Member

topi314 commented Apr 8, 2026

possible better fix: #537

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

3 participants