{"record":{"id":"affa37e3f1e266cb","repo":"juanfont/headscale","slug":"saving-node-to-database-w","errorCode":null,"errorMessage":"saving node to database: %w","messagePattern":"saving node to database: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/db/node.go","lineNumber":335,"sourceCode":"\t\t\tStr(zf.NodeKey, node.NodeKey.ShortString()).\n\t\t\tStr(zf.UserName, node.User.Username()).\n\t\t\tMsg(\"Test node authorized again\")\n\n\t\treturn &node, nil\n\t}\n\n\tnode.IPv4 = ipv4\n\tnode.IPv6 = ipv6\n\n\tif node.GivenName == \"\" {\n\t\tnode.GivenName = dnsname.SanitizeHostname(node.Hostname)\n\t\tif node.GivenName == \"\" {\n\t\t\tnode.GivenName = \"node\"\n\t\t}\n\t}\n\n\tif err := tx.Save(&node).Error; err != nil { //nolint:noinlineerr\n\t\treturn nil, fmt.Errorf(\"saving node to database: %w\", err)\n\t}\n\n\tlog.Trace().\n\t\tCaller().\n\t\tStr(zf.NodeHostname, node.Hostname).\n\t\tMsg(\"Test node registered with the database\")\n\n\treturn &node, nil\n}\n\n// NodeSetNodeKey sets the node key of a node and saves it to the database.\nfunc NodeSetNodeKey(tx *gorm.DB, node *types.Node, nodeKey key.NodePublic) error {\n\treturn tx.Model(node).Updates(types.Node{\n\t\tNodeKey: nodeKey,\n\t}).Error\n}\n\nfunc (hsdb *HSDatabase) NodeSetMachineKey(","sourceCodeStart":317,"sourceCodeEnd":353,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/node.go#L317-L353","documentation":"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.","triggerScenarios":"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).","commonSituations":"Race between interactive auth callback and client poll; automation pre-creating rows; partial migration leaving constraints inconsistent.","solutions":["Unwrap the driver error to identify the violated constraint","Ensure the user record exists and is committed before registering the node","For duplicate-machine-key races, retry registration so the existing-node path handles it","Fix underlying DB health if the error is lock/connectivity"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Ensure the owning user row is committed before registering\nif _, err := db.GetUserByID(uid); err != nil {\n\treturn nil, fmt.Errorf(\"cannot register node for missing user: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"if _, err := db.RegisterNode(tx, node); err != nil {\n\tif isUniqueViolation(err) || isTransientDBError(err) {\n\t\t// retryable: race or lock\n\t}\n\treturn err\n}","preventionTips":["Commit user creation before node registration","Prefer servertest harness to reproduce registration races","Alert on registration-path 5xx to catch schema drift early"],"tags":["go","database","registration","gorm","constraints"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}