{"record":{"id":"38bc221320cb553a","repo":"juanfont/headscale","slug":"updating-tags-for-node-d-w","errorCode":null,"errorMessage":"updating tags for node %d: %w","messagePattern":"updating tags for node (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/db/db.go","lineNumber":686,"sourceCode":"\t\t\t\t\t\t\t\t\tStrs(\"rejected_tags\", rejectedTags).\n\t\t\t\t\t\t\t\t\tMsg(\"RequestTags rejected during migration (not authorized)\")\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tmergedTags := append(slices.Clone(existingTags), validatedTags...)\n\t\t\t\t\t\tslices.Sort(mergedTags)\n\t\t\t\t\t\tmergedTags = slices.Compact(mergedTags)\n\n\t\t\t\t\t\ttagsJSON, err := json.Marshal(mergedTags)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"serializing merged tags for node %d: %w\", node.ID, err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\terr = tx.Exec(\"UPDATE nodes SET tags = ? WHERE id = ?\", string(tagsJSON), node.ID).Error\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"updating tags for node %d: %w\", node.ID, err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tlog.Info().\n\t\t\t\t\t\t\tEmbedObject(node).\n\t\t\t\t\t\t\tStrs(\"validated_tags\", validatedTags).\n\t\t\t\t\t\t\tStrs(\"rejected_tags\", rejectedTags).\n\t\t\t\t\t\t\tStrs(\"existing_tags\", existingTags).\n\t\t\t\t\t\t\tStrs(\"merged_tags\", mergedTags).\n\t\t\t\t\t\t\tMsg(\"Migrated validated RequestTags from host_info to tags column\")\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tRollback: func(db *gorm.DB) error { return nil },\n\t\t\t},\n\t\t\t{\n\t\t\t\t// Clear user_id on tagged nodes.\n\t\t\t\t// Tagged nodes are owned by their tags, not a user.","sourceCodeStart":668,"sourceCodeEnd":704,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/db.go#L668-L704","documentation":"The RequestTags migration fails its raw UPDATE nodes SET tags = ? WHERE id = ? write after merging validated RequestTags. Causes: the tags column missing (preceding rename migration inconsistent), a type constraint on tags in drifted schemas, lock timeout, or - on Postgres - the migration transaction already being aborted by an earlier statement error, after which every subsequent UPDATE fails with 'current transaction is aborted'.","triggerScenarios":"Writing merged tags for a node while the nodes table is locked by another session, the tags column does not exist due to schema drift from the 202511131445 rename, or a Postgres transaction already aborted by a prior error so this UPDATE fails with 'current transaction is aborted'.","commonSituations":"Upgrading under peer load with nodes actively checking in; databases restored with mismatched schema; diagnosing only the last error of an aborted transaction instead of the first.","solutions":["Read the wrapped error first: 'no such column: tags' means fix schema/history ordering per the rename migration; 'database is locked' means serialize access; 'transaction is aborted' means find the earlier failing statement in the same log","Stop all other headscale instances and admin clients, restart headscale so the migration re-runs atomically","Verify host_info JSON for the node ID in the message is well-formed if the error mentions JSON","Keep regular backups so a failed migration window can be replayed from a clean snapshot"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Ensure exclusive write access before the upgrade window (Postgres)\nrows, _ := db.Query(`SELECT count(*) FROM pg_stat_activity\n\tWHERE query ILIKE '%nodes%' AND pid <> pg_backend_pid()`)\nvar writers int\nif rows.Next() { rows.Scan(&writers) }\nif writers > 0 {\n\tlog.Warnf(\"%d sessions touching nodes; migration UPDATE may block\", writers)\n}","typeGuard":null,"tryCatchPattern":"// Startup-level retry: transient lock timeouts resolve on a clean second boot\nfor attempt := 1; attempt <= 3; attempt++ {\n\terr := runMigrations(db)\n\tif err == nil { break }\n\tif !strings.Contains(err.Error(), \"locked\") || attempt == 3 {\n\t\tlog.Fatal().Err(err).Msg(\"migration failed\")\n\t}\n\ttime.Sleep(time.Duration(attempt) * 5 * time.Second)\n}","preventionTips":["Stop all writers (peers via maintenance, admin CLI/API) during upgrades","On Postgres, remember transaction-abort semantics: diagnose the first error, not this one","Verify the tags column exists before upgrading across the 0.26 to 0.27+ boundary"],"tags":["database","migration","nodes","tags","locking"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}