juanfont/headscale · error

failed to convert node interface

Error message

failed to convert node interface

What it means

Internal sentinel in hscontrol/db/node.go used when converting the GORM node model to/from the types layer fails. The database stores MachineKey/NodeKey as serialized text; if a row contains a key that cannot be deserialized (wrong length, non-hex), the conversion aborts with this error.

Source

Thrown at hscontrol/db/node.go:49

// ErrNodeNameNotUnique is returned when a node name is not unique.
var ErrNodeNameNotUnique = errors.New("node name is not unique")

// preloadNode returns a session that eager-loads a node's AuthKey, the
// AuthKey's User, and the node's User.
func preloadNode(tx *gorm.DB) *gorm.DB {
	return tx.
		Preload("AuthKey").
		Preload("AuthKey.User").
		Preload("User")
}

var (
	ErrNodeNotFound                  = errors.New("node not found")
	ErrNodeRouteIsNotAvailable       = errors.New("route is not available on node")
	ErrNodeNotFoundRegistrationCache = errors.New(
		"node not found in registration cache",
	)
	ErrCouldNotConvertNodeInterface = errors.New("failed to convert node interface")
)

// ListPeers returns peers of node, regardless of any Policy or if the node is expired.
// If no peer IDs are given, all peers are returned.
// If at least one peer ID is given, only these peer nodes will be returned.
func (hsdb *HSDatabase) ListPeers(nodeID types.NodeID, peerIDs ...types.NodeID) (types.Nodes, error) {
	return ListPeers(hsdb.DB, nodeID, peerIDs...)
}

// ListPeers returns peers of node, regardless of any Policy or if the node is expired.
// If no peer IDs are given, all peers are returned.
// If at least one peer ID is given, only these peer nodes will be returned.
func ListPeers(tx *gorm.DB, nodeID types.NodeID, peerIDs ...types.NodeID) (types.Nodes, error) {
	nodes := types.Nodes{}

	err := preloadNode(tx).
		Where("id <> ?", nodeID).
		Where(peerIDs).Find(&nodes).Error

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Inspect the offending row's key columns (machine_key, node_key) for invalid length/characters
  2. Delete or fix the corrupted node row (the node will re-register and rewrite its keys)
  3. Avoid direct SQL edits to node rows; use the CLI/API
Defensive patterns

Strategy: try-catch

Try / catch

node, err := db.GetNode(tx, id)
if err != nil {
    if errors.Is(err, db.ErrCouldNotConvertNodeInterface) {
        // row-level corruption: quarantine the node, force re-registration
        log.Error().Uint64("node", uint64(id)).Msg("unparseable node row; deleting so it can re-register")
        return db.DeleteNode(tx, id)
    }
    return err
}

Prevention

When it happens

Trigger: A nodes row whose machine_key/node_key column holds malformed data — hand-edited DB, a row written by an incompatible older version, or a truncated key string. Encountered on node load/save paths that round-trip key material.

Common situations: Manual SQL surgery on the nodes table; restoring a database across major version gaps; schema drift where the text serialiser format changed.

Related errors


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