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).ErrorView on GitHub (pinned to 565fd254d0)
Solutions
- Inspect the offending row's key columns (machine_key, node_key) for invalid length/characters
- Delete or fix the corrupted node row (the node will re-register and rewrite its keys)
- 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
- Never hand-edit node rows in SQL; use the CLI/API
- Test database restores in a staging instance before pointing production at them
- Keep headscale versions adjacent when migrating databases (avoid multi-version jumps)
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
- node not found
- database type not supported
- foreign key constraints violated
- node name is not unique
- route is not available on node
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/9787171d48bbee65.
Report an issue: GitHub.