{"record":{"id":"9787171d48bbee65","repo":"juanfont/headscale","slug":"failed-to-convert-node-interface","errorCode":null,"errorMessage":"failed to convert node interface","messagePattern":"failed to convert node interface","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/db/node.go","lineNumber":49,"sourceCode":"// ErrNodeNameNotUnique is returned when a node name is not unique.\nvar ErrNodeNameNotUnique = errors.New(\"node name is not unique\")\n\n// preloadNode returns a session that eager-loads a node's AuthKey, the\n// AuthKey's User, and the node's User.\nfunc preloadNode(tx *gorm.DB) *gorm.DB {\n\treturn tx.\n\t\tPreload(\"AuthKey\").\n\t\tPreload(\"AuthKey.User\").\n\t\tPreload(\"User\")\n}\n\nvar (\n\tErrNodeNotFound                  = errors.New(\"node not found\")\n\tErrNodeRouteIsNotAvailable       = errors.New(\"route is not available on node\")\n\tErrNodeNotFoundRegistrationCache = errors.New(\n\t\t\"node not found in registration cache\",\n\t)\n\tErrCouldNotConvertNodeInterface = errors.New(\"failed to convert node interface\")\n)\n\n// ListPeers returns peers of node, regardless of any Policy or if the node is expired.\n// If no peer IDs are given, all peers are returned.\n// If at least one peer ID is given, only these peer nodes will be returned.\nfunc (hsdb *HSDatabase) ListPeers(nodeID types.NodeID, peerIDs ...types.NodeID) (types.Nodes, error) {\n\treturn ListPeers(hsdb.DB, nodeID, peerIDs...)\n}\n\n// ListPeers returns peers of node, regardless of any Policy or if the node is expired.\n// If no peer IDs are given, all peers are returned.\n// If at least one peer ID is given, only these peer nodes will be returned.\nfunc ListPeers(tx *gorm.DB, nodeID types.NodeID, peerIDs ...types.NodeID) (types.Nodes, error) {\n\tnodes := types.Nodes{}\n\n\terr := preloadNode(tx).\n\t\tWhere(\"id <> ?\", nodeID).\n\t\tWhere(peerIDs).Find(&nodes).Error","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/node.go#L31-L67","documentation":"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.","triggerScenarios":"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.","commonSituations":"Manual SQL surgery on the nodes table; restoring a database across major version gaps; schema drift where the text serialiser format changed.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"node, err := db.GetNode(tx, id)\nif err != nil {\n    if errors.Is(err, db.ErrCouldNotConvertNodeInterface) {\n        // row-level corruption: quarantine the node, force re-registration\n        log.Error().Uint64(\"node\", uint64(id)).Msg(\"unparseable node row; deleting so it can re-register\")\n        return db.DeleteNode(tx, id)\n    }\n    return err\n}","preventionTips":["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)"],"tags":["node","database","serialization","keys","headscale"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}