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.groupView on GitHub (pinned to b6b368adc5)
Solutions
- Treat the peer as broken/malicious; report to the server operator.
- Log and fail the connection — there is no client-side workaround that preserves security.
- Verify against a different server hostname/IP to rule out a route-specific middlebox.
- 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
- Maintain a blocklist of peers that emit non-conformant handshakes.
- Do not auto-retry on protocol-violation errors; they indicate a broken or hostile peer.
- Capture handshakes for forensic review when these errors cluster.
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
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- tls: server sent an unnecessary HelloRetryRequest key_share
- tls: server sent a cookie in a normal ServerHello
- tls: malformed key_share extension
- tls: server did not send a key share
- tls: server selected an invalid PSK
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/fd415556df5bca32.
Report an issue: GitHub.