juanfont/headscale · error

saving node to database: %w

Error message

saving node to database: %w

What it means

The save for a node taking the fresh-registration path (it had no existing IPs): after assigning ipv4/ipv6 and defaulting GivenName, tx.Save persists the new node row. Failure means the INSERT/UPDATE was rejected — unique constraint on machine key, node key, or given name, or a database availability error.

Source

Thrown at hscontrol/db/node.go:335

			Str(zf.NodeKey, node.NodeKey.ShortString()).
			Str(zf.UserName, node.User.Username()).
			Msg("Test node authorized again")

		return &node, nil
	}

	node.IPv4 = ipv4
	node.IPv6 = ipv6

	if node.GivenName == "" {
		node.GivenName = dnsname.SanitizeHostname(node.Hostname)
		if node.GivenName == "" {
			node.GivenName = "node"
		}
	}

	if err := tx.Save(&node).Error; err != nil { //nolint:noinlineerr
		return nil, fmt.Errorf("saving node to database: %w", err)
	}

	log.Trace().
		Caller().
		Str(zf.NodeHostname, node.Hostname).
		Msg("Test node registered with the database")

	return &node, nil
}

// NodeSetNodeKey sets the node key of a node and saves it to the database.
func NodeSetNodeKey(tx *gorm.DB, node *types.Node, nodeKey key.NodePublic) error {
	return tx.Model(node).Updates(types.Node{
		NodeKey: nodeKey,
	}).Error
}

func (hsdb *HSDatabase) NodeSetMachineKey(

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Unwrap the driver error to identify the violated constraint
  2. Ensure the user record exists and is committed before registering the node
  3. For duplicate-machine-key races, retry registration so the existing-node path handles it
  4. Fix underlying DB health if the error is lock/connectivity
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure the owning user row is committed before registering
if _, err := db.GetUserByID(uid); err != nil {
	return nil, fmt.Errorf("cannot register node for missing user: %w", err)
}

Try / catch

if _, err := db.RegisterNode(tx, node); err != nil {
	if isUniqueViolation(err) || isTransientDBError(err) {
		// retryable: race or lock
	}
	return err
}

Prevention

When it happens

Trigger: Same machine registering twice concurrently; expired node re-registering with a key that now duplicates another row; DB lock/timeout; NOT NULL or foreign-key violation (e.g. user id not yet committed).

Common situations: Race between interactive auth callback and client poll; automation pre-creating rows; partial migration leaving constraints inconsistent.

Related errors


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