t8y2/dbx · error
ZooKeeper DIGEST-MD5 completed with an unexpected client tok
Error message
ZooKeeper DIGEST-MD5 completed with an unexpected client token
What it means
This error is thrown by negotiateSASLDigest when the DIGEST-MD5 SASL client reports Complete() but Step() still returned a non-empty token. A completed DIGEST-MD5 exchange must produce a final empty client token; a non-empty one means the negotiated state machine ended in an inconsistent state.
Source
Thrown at agents/drivers/zookeeper/sasl.go:111
}
for round := 0; round < zooKeeperSASLMaxRounds; round++ {
challenge, err := zooKeeperSASLRound(connection, zooKeeperSASLXIDBase+int32(round), token)
if err != nil {
return fmt.Errorf("ZooKeeper SASL round %d: %w", round+1, err)
}
if saslClient.Complete() {
if len(challenge) != 0 {
return errors.New("ZooKeeper sent an unexpected token after DIGEST-MD5 completion")
}
return nil
}
token, err = saslClient.Step(challenge)
if err != nil {
return fmt.Errorf("continue ZooKeeper DIGEST-MD5 negotiation at round %d: %w", round+1, err)
}
if saslClient.Complete() {
if len(token) != 0 {
return errors.New("ZooKeeper DIGEST-MD5 completed with an unexpected client token")
}
return nil
}
}
return fmt.Errorf("ZooKeeper DIGEST-MD5 negotiation exceeded %d rounds", zooKeeperSASLMaxRounds)
}
func zooKeeperSASLRound(connection net.Conn, xid int32, token []byte) ([]byte, error) {
payload := make([]byte, 12+len(token))
binary.BigEndian.PutUint32(payload[0:4], uint32(xid))
binary.BigEndian.PutUint32(payload[4:8], uint32(zooKeeperSASLOpcode))
binary.BigEndian.PutUint32(payload[8:12], uint32(len(token)))
copy(payload[12:], token)
if err := writeZooKeeperFrame(connection, payload); err != nil {
return nil, err
}
response, err := readZooKeeperFrame(connection)
if err != nil {View on GitHub (pinned to c0390bff16)
Solutions
- Verify the SASL client implementation (e.g. the DIGEST-MD5 library behind saslClient) emits an empty final token on completion
- Check that the server is a standard ZooKeeper peer and not a proxy rewriting SASL frames
- Compare the server's final challenge against a known-good ZooKeeper SASL handshake capture
- Update the driver and SASL dependency versions to aligned, tested releases
Defensive patterns
Strategy: try-catch
Validate before calling
// Go: pre-check SASL client behavior with a test harness
if saslClient.Complete() && len(lastToken) != 0 {
// abort handshake before sending; treat client implementation as buggy
} Try / catch
if err := negotiateSASLDigest(conn, saslClient); err != nil {
if strings.Contains(err.Error(), "unexpected client token") {
// log full handshake round trace; fall back to non-SASL auth or fail fast
return fmt.Errorf("zookeeper SASL implementation mismatch: %w", err)
}
return err
} Prevention
- Pin tested versions of the SASL client library and driver together
- Add a unit test asserting the final DIGEST-MD5 step returns an empty token
- Log each Step() round length during integration testing
- Verify against a stock ZooKeeper server before custom deployments
When it happens
Trigger: During ZooKeeper SASL DIGEST-MD5 authentication, a server challenge causes saslClient.Step(challenge) to return a token in the same round where saslClient.Complete() becomes true.
Common situations: Mismatched or buggy SASL client implementations, a server sending an extra final challenge with data, or a nonstandard ZooKeeper/quorum peer that appends data to the last DIGEST-MD5 step.
Related errors
- ZooKeeper sent an unexpected token after DIGEST-MD5 completi
- ZooKeeper DIGEST-MD5 negotiation exceeded %d rounds
- ZooKeeper session closed because SASL authentication is requ
- ZooKeeper sent an unexpected token after GSSAPI completion
- ZooKeeper authentication failed
AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05).
Data as JSON: /api/errors/5c78d47f259fcd27.
Report an issue: GitHub.