juanfont/headscale · error

checking name uniqueness: %w

Error message

checking name uniqueness: %w

What it means

RenameNode counts existing nodes with the target given_name to enforce uniqueness before writing. This error is the COUNT query itself failing at the database level (not a duplicate — that returns ErrNodeNameNotUnique). The underlying driver error is in %w.

Source

Thrown at hscontrol/db/node.go:197

func SetLastSeen(tx *gorm.DB, nodeID types.NodeID, lastSeen time.Time) error {
	return tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("last_seen", lastSeen).Error
}

// RenameNode takes a [types.Node] struct and a new [types.Node.GivenName] for the nodes
// and renames it. Validation should be done in the state layer before calling this function.
func RenameNode(tx *gorm.DB,
	nodeID types.NodeID, newName string,
) error {
	err := dnsname.ValidLabel(newName)
	if err != nil {
		return fmt.Errorf("renaming node: %w", err)
	}

	// Check if the new name is unique
	var count int64

	if err := tx.Model(&types.Node{}).Where("given_name = ? AND id != ?", newName, nodeID).Count(&count).Error; err != nil { //nolint:noinlineerr
		return fmt.Errorf("checking name uniqueness: %w", err)
	}

	if count > 0 {
		return ErrNodeNameNotUnique
	}

	if err := tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("given_name", newName).Error; err != nil { //nolint:noinlineerr
		return fmt.Errorf("renaming node in database: %w", err)
	}

	return nil
}

func (hsdb *HSDatabase) NodeSetExpiry(nodeID types.NodeID, expiry *time.Time) error {
	return hsdb.Write(func(tx *gorm.DB) error {
		return NodeSetExpiry(tx, nodeID, expiry)
	})
}

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Unwrap the error to distinguish lock/connectivity/schema causes
  2. For SQLite contention, retry after the competing write completes or enable busy_timeout
  3. Run pending migrations and verify the nodes table schema
  4. Retry the rename once the database is healthy
Defensive patterns

Strategy: retry

Try / catch

if err := db.RenameNode(tx, nodeID, name); err != nil {
	if isTransientDBError(err) {
		// retry after competing write completes
	}
	return err
}

Prevention

When it happens

Trigger: SQLite database locked by a concurrent rename/registration; Postgres connection reset; schema drift where given_name column is missing after a botched migration.

Common situations: Bulk renames via automation hitting SQLite write contention; interrupted upgrade leaving the schema half-migrated.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/0a03e0ebcdb5ecbb. Report an issue: GitHub.