golang/go · error

tls: server changed cipher suite after a HelloRetryRequest

Error message

tls: server changed cipher suite after a HelloRetryRequest

What it means

Thrown in checkServerHelloOrHRR() when the server selects a different cipher suite in the final ServerHello than it did in the HelloRetryRequest. RFC 8446 section 4.1.4 requires the cipher suite to be consistent between the HRR and the final ServerHello.

Source

Thrown at src/crypto/tls/handshake_client_tls13.go:207

		len(hs.serverHello.scts) != 0 {
		c.sendAlert(alertUnsupportedExtension)
		return errors.New("tls: server sent a ServerHello extension forbidden in TLS 1.3")
	}

	if !bytes.Equal(hs.hello.sessionId, hs.serverHello.sessionId) {
		c.sendAlert(alertIllegalParameter)
		return errors.New("tls: server did not echo the legacy session ID")
	}

	if hs.serverHello.compressionMethod != compressionNone {
		c.sendAlert(alertDecodeError)
		return errors.New("tls: server sent non-zero legacy TLS compression method")
	}

	selectedSuite := mutualCipherSuiteTLS13(hs.hello.cipherSuites, hs.serverHello.cipherSuite)
	if hs.suite != nil && selectedSuite != hs.suite {
		c.sendAlert(alertIllegalParameter)
		return errors.New("tls: server changed cipher suite after a HelloRetryRequest")
	}
	if selectedSuite == nil {
		c.sendAlert(alertIllegalParameter)
		return errors.New("tls: server chose an unconfigured cipher suite")
	}
	hs.suite = selectedSuite
	c.cipherSuite = hs.suite.id

	return nil
}

// sendDummyChangeCipherSpec sends a ChangeCipherSpec record for compatibility
// with middleboxes that didn't implement TLS correctly. See RFC 8446, Appendix D.4.
func (hs *clientHandshakeStateTLS13) sendDummyChangeCipherSpec() error {
	if hs.c.quic != nil {
		return nil
	}
	if hs.sentDummyCCS {

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Server must keep the same cipher suite in both the HelloRetryRequest and the final ServerHello per RFC 8446.
  2. If using a load balancer, ensure session affinity routes both HRR and the final handshake to the same backend.
  3. Report as a server-side TLS 1.3 implementation bug.
  4. As a workaround, restrict client to TLS 1.2 to avoid HRR entirely.
Defensive patterns

Strategy: try-catch

Try / catch

conn, err := tls.Dial("tcp", addr, config)
if err != nil {
    if strings.Contains(err.Error(), "changed cipher suite after a HelloRetryRequest") {
        // Server bug — ensure session affinity or fall back to TLS 1.2
        config.MaxVersion = tls.VersionTLS12
        conn, err = tls.Dial("tcp", addr, config)
    }
}

Prevention

When it happens

Trigger: Triggered when hs.suite is already set (from a prior HelloRetryRequest) and mutualCipherSuiteTLS13() returns a different suite than hs.suite. The server changed its cipher suite selection mid-handshake.

Common situations: Server bug in HelloRetryRequest handling where the HRR and final ServerHello are processed by different code paths. Load balancer routing the HRR and final ServerHello to different backend servers. Server that recomputes cipher suite selection between HRR and the final handshake message.

Understand the failure class

Related errors


AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12). Data as JSON: /api/errors/dc2d5073aa094542. Report an issue: GitHub.