golang/go · error

tls: server sent two HelloRetryRequest messages

Error message

tls: server sent two HelloRetryRequest messages

What it means

processServerHello compares the server_random against the canonical helloRetryRequestRandom sentinel. RFC 8446 §4.1.3 permits at most one HelloRetryRequest per handshake; a second random equal to the HRR sentinel means the server sent a second HRR. Go sends `unexpected_message` and aborts. This is a hard protocol violation.

Source

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

		c.sendAlert(alertUnexpectedMessage)
		return unexpectedMessageError(serverHello, msg)
	}
	hs.serverHello = serverHello

	if err := hs.checkServerHelloOrHRR(); err != nil {
		return err
	}

	c.didHRR = true
	return nil
}

func (hs *clientHandshakeStateTLS13) processServerHello() error {
	c := hs.c

	if bytes.Equal(hs.serverHello.random, helloRetryRequestRandom) {
		c.sendAlert(alertUnexpectedMessage)
		return errors.New("tls: server sent two HelloRetryRequest messages")
	}

	if len(hs.serverHello.cookie) != 0 {
		c.sendAlert(alertUnsupportedExtension)
		return errors.New("tls: server sent a cookie in a normal ServerHello")
	}

	if hs.serverHello.selectedGroup != 0 {
		c.sendAlert(alertDecodeError)
		return errors.New("tls: malformed key_share extension")
	}

	if hs.serverHello.serverShare.group == 0 {
		c.sendAlert(alertIllegalParameter)
		return errors.New("tls: server did not send a key share")
	}
	if !slices.ContainsFunc(hs.hello.keyShares, func(ks keyShare) bool {
		return ks.group == hs.serverHello.serverShare.group

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Treat the peer as broken/malicious; report to the server operator.
  2. Log and fail the connection — there is no client-side workaround that preserves security.
  3. Verify against a different server hostname/IP to rule out a route-specific middlebox.
  4. If you operate the server, ensure HelloRetryRequest is sent at most once per handshake.
Defensive patterns

Strategy: try-catch

Try / catch

// Second HelloRetryRequest is a protocol violation; abort permanently for this peer.
conn, err := tls.Dial("tcp", addr, cfg)
if err != nil {
    if strings.Contains(err.Error(), "two HelloRetryRequest") {
        blocklist.Add(addr) // server is broken or malicious
    }
    return err
}

Prevention

When it happens

Trigger: After already processing one HelloRetryRequest (hs.didHRR == true via the earlier flow), the subsequent ServerHello's random again equals helloRetryRequestRandom. Produced by a server that loops on HRR or replays it.

Common situations: Faulty server implementations of TLS 1.3 retry logic, adversarial fuzzers, or stateful MITM proxies that re-issue HRR. No legitimate mainstream server does this.

Understand the failure class

Related errors


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