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

  1. Verify the SASL client implementation (e.g. the DIGEST-MD5 library behind saslClient) emits an empty final token on completion
  2. Check that the server is a standard ZooKeeper peer and not a proxy rewriting SASL frames
  3. Compare the server's final challenge against a known-good ZooKeeper SASL handshake capture
  4. 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

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


AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05). Data as JSON: /api/errors/5c78d47f259fcd27. Report an issue: GitHub.