t8y2/dbx · error

parse ZooKeeper TLS address %q: %w

Error message

parse ZooKeeper TLS address %q: %w

What it means

Wrapped during a TLS connection to a ZooKeeper ensemble: when the tls.Config has no explicit ServerName, the client derives it from the address via net.SplitHostPort. If the address is malformed (no host:port, brackets wrong, extra characters), SplitHostPort fails and the dial aborts with this error. It surfaces the underlying parse error plus the offending address string.

Source

Thrown at agents/drivers/argo-go/zookeeper_protocol.go:59

var newZooKeeperSASLClient = func(host string, config connectionConfig) (zooKeeperSASLClient, error) {
	service, options := zooKeeperGSSAPIOptions(config)
	mechanism, err := gosasl.NewGSSAPIMechanismWithOptions(service, options)
	if err != nil {
		return nil, err
	}
	return gosasl.NewSaslClient(host, mechanism), nil
}

var dialZooKeeperConnection = func(address string, timeout time.Duration, tlsConfig *tls.Config) (net.Conn, error) {
	dialer := &net.Dialer{Timeout: timeout}
	if tlsConfig == nil {
		return dialer.Dial("tcp", address)
	}
	config := tlsConfig.Clone()
	if config.ServerName == "" {
		host, _, err := net.SplitHostPort(address)
		if err != nil {
			return nil, fmt.Errorf("parse ZooKeeper TLS address %q: %w", address, err)
		}
		config.ServerName = host
	}
	return tls.DialWithDialer(dialer, "tcp", address, config)
}

var shuffleZooKeeperServers = func(servers []string) {
	rand.Shuffle(len(servers), func(first, second int) {
		servers[first], servers[second] = servers[second], servers[first]
	})
}

func zooKeeperGSSAPIOptions(config connectionConfig) (string, gosasl.GSSAPIOptions) {
	service := firstNonEmpty(config.ZooKeeperKerberos.Service, "zookeeper")
	options := gssapiOptionsFromKerberos(config.Kerberos)
	options.QOP = "auth"
	options.AuthorizationID = ""
	options.ServiceHost = ""

View on GitHub (pinned to c0390bff16)

Solutions

  1. Correct the address to valid 'host:port' form (e.g. 'zk1.example.com:2181', '[2001:db8::1]:2181')
  2. If the config intentionally omits the port, append the default ZooKeeper port 2181 before dialing
  3. Alternatively set tlsConfig.ServerName explicitly in code so the SplitHostPort fallback is skipped
  4. Validate addresses with net.SplitHostPort at config load time and fail early with a clear message

Example fix

// before
address := "zk1.example.com"
conn, err := tlsConnect(address)
// after
address := "zk1.example.com:2181"
if _, _, err := net.SplitHostPort(address); err != nil {
    return fmt.Errorf("invalid ZooKeeper address %q: %w", address, err)
}
conn, err := tlsConnect(address)
Defensive patterns

Strategy: validation

Validate before calling

func validZKAddress(addr string) error {
    _, _, err := net.SplitHostPort(addr)
    return err
}

Type guard

func isHostPort(s string) bool {
    host, port, err := net.SplitHostPort(s)
    return err == nil && host != "" && port != ""
}

Try / catch

conn, err := tlsConnect(address)
if err != nil {
    var parseErr *net.AddrError
    if errors.As(err, &parseErr) && parseErr.Err == "missing port in address" {
        address = net.JoinHostPort(strings.TrimSuffix(address, ":"), "2181")
        conn, err = tlsConnect(address)
    }
    if err != nil { return err }
}

Prevention

When it happens

Trigger: Calling the TLS dial path in zookeeper_protocol.go with an address string that net.SplitHostPort cannot parse — e.g. missing port ('zk1:'), an empty string, an unbalanced IPv6 literal ('[::1'), or a host containing stray characters before TLS dial when config.ServerName is empty.

Common situations: ZooKeeper connect string assembled by hand or from a misparsed config value; trimming the port accidentally when substituting hosts; IPv6 ensemble addresses not bracketed; environment variable containing a bare hostname without ':2181'.

Understand the failure class

Related errors


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