AlexxIT/go2rtc · error

hap: can't dial witout client_id or client_private

Error message

hap: can't dial witout client_id or client_private

What it means

Client.Dial refuses to start HAP session establishment when the client's pairing identity is not configured: either ClientID or ClientPrivate (the client's ed25519 ID and private key from a previous pairing) is empty. Dial needs these to sign the session and authenticate to the accessory.

Solutions

  1. Set ClientID and ClientPrivate before calling Dial — load them from persisted pairing state or pair the device first.
  2. Check the config file/struct mapping: the JSON keys client_id and client_private must actually populate the fields.
  3. If no pairing exists yet, run the Pair flow first to establish the client identity, then persist it.
  4. Log the lengths of both fields before Dial to confirm they are non-empty at runtime.

Example fix

// before: client identity missing -> Dial fails
client := &hap.Client{DeviceID: deviceID, DeviceAddress: addr}
err := client.Dial()
// after: load persisted identity first
client := &hap.Client{
    DeviceID: deviceID, DeviceAddress: addr,
    ClientID:      cfg.HAP.ClientID,       // non-empty
    ClientPrivate: cfg.HAP.ClientPrivate,  // non-empty ed25519 private key
}
if len(client.ClientID) == 0 || len(client.ClientPrivate) == 0 {
    return errors.New("pair the device first; client identity missing")
}
err := client.Dial()
Defensive patterns

Strategy: validation

Validate before calling

if len(client.ClientID) == 0 || len(client.ClientPrivate) == 0 {
    return errors.New("client identity missing: pair the device first and persist client_id/client_private")
}

Try / catch

if err := client.Dial(); err != nil {
    if strings.Contains(err.Error(), "witout client_id") {
        return fmt.Errorf("configuration error: run the pairing setup to create the client identity: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Constructing a Client with only device fields (DeviceID/DeviceAddress) and forgetting to load the client identity; loading persisted pairing state from a config file that is missing the client_id/client_private fields; creating a fresh Client for a device that was already paired elsewhere.

Common situations: Config file missing the client identity keys after a fresh install or migration; JSON unmarshalling silently leaving the fields empty due to wrong key names; running the client on a new host without copying the pairing credentials.

Related errors


AI-assisted analysis of AlexxIT/go2rtc@c245815e75 (2026-09-07). Data as JSON: /api/errors/555fb5e3e2af167d. Report an issue: GitHub.

Appendix: source

Thrown at pkg/hap/client.go:90

}

func (c *Client) URL() string {
	return fmt.Sprintf(
		"homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x",
		c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate,
	)
}

func (c *Client) DeviceHost() string {
	if i := strings.IndexByte(c.DeviceAddress, ':'); i > 0 {
		return c.DeviceAddress[:i]
	}
	return c.DeviceAddress
}

func (c *Client) Dial() (err error) {
	if len(c.ClientID) == 0 || len(c.ClientPrivate) == 0 {
		return errors.New("hap: can't dial witout client_id or client_private")
	}

	// update device address (host and/or port) before dial
	_ = mdns.QueryOrDiscovery(c.DeviceHost(), mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
		if entry.Complete() && entry.Info["id"] == c.DeviceID {
			c.DeviceAddress = entry.Addr()
			return true
		}
		return false
	})

	// TODO: close conn on error
	if c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil {
		return
	}

	c.reader = bufio.NewReader(c.Conn)

View on GitHub (pinned to c245815e75)