{"record":{"id":"b078bb5cdefeb972","repo":"juanfont/headscale","slug":"node-marked-as-tagged-but-has-no-tags","errorCode":null,"errorMessage":"node marked as tagged but has no tags","messagePattern":"node marked as tagged but has no tags","errorType":"http","errorClass":"ErrNodeMarkedTaggedButHasNoTags","httpStatus":400,"severity":"error","filePath":"hscontrol/state/tags.go","lineNumber":13,"sourceCode":"package state\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"github.com/rs/zerolog/log\"\n)\n\nvar (\n\t// ErrNodeMarkedTaggedButHasNoTags is returned when a node is marked as tagged but has no tags.\n\tErrNodeMarkedTaggedButHasNoTags = errors.New(\"node marked as tagged but has no tags\")\n\n\t// ErrNodeHasNeitherUserNorTags is returned when a node has neither a user nor tags.\n\tErrNodeHasNeitherUserNorTags = errors.New(\"node has neither user nor tags - must be owned by user or tagged\")\n\n\t// ErrRequestedTagsInvalidOrNotPermitted is returned when requested tags are invalid or not permitted.\n\t// This message format matches Tailscale SaaS: \"requested tags [tag:xxx] are invalid or not permitted\".\n\tErrRequestedTagsInvalidOrNotPermitted = errors.New(\"requested tags\")\n)\n\n// ErrTaggedNodeHasUser is returned when a tagged node has a [types.Node.UserID] set.\nvar ErrTaggedNodeHasUser = errors.New(\"tagged node must not have user_id set\")\n\n// validateNodeOwnership ensures proper node ownership model.\n// A node must be either user-owned or tagged, and these are mutually exclusive:\n// tagged nodes must not have a [types.Node.UserID], and user-owned nodes must\n// not have tags.\nfunc validateNodeOwnership(node *types.Node) error {\n\tif node.IsTagged() {","sourceCodeStart":1,"sourceCodeEnd":31,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/state/tags.go#L1-L31","documentation":"ErrNodeMarkedTaggedButHasNoTags is a sentinel error in hscontrol/state/tags.go:13 returned by validateNodeOwnership when a node's IsTagged() is true but its Tags slice is empty. It guards the tags-XOR-user-ownership invariant: a tagged node must carry at least one tag.","triggerScenarios":"Constructing or persisting a types.Node with a tagged owner marker but an empty Tags list; SetTags clearing tags without clearing the tagged marker; tests building node fixtures that set the tagged flag without tags.","commonSituations":"Manual construction of Node structs in code or tests bypassing SetTags validation; partial DB updates that empty the tags column but leave the ownership marker; import scripts writing nodes directly.","solutions":["Ensure the node gets at least one valid tag before it is marked tagged (use state.SetTags, not direct field writes)","If the intent is a user-owned node, clear the tagged marker instead of emptying Tags","Fix test fixtures to include a tag when exercising tagged-node paths","Audit direct DB writes / import scripts that bypass ownership validation"],"exampleFix":"// before\nnode := types.Node{MachineKey: mk, NodeKey: nk} // tagged marker set, Tags empty\n\n// after\n// go through state.SetTags(node, []string{\"tag:web\"}) so ownership stays consistent","handlingStrategy":"validation","validationCode":"func hasValidOwnership(n types.NodeView) error {\n    if n.IsTagged() && len(n.Tags()) == 0 {\n        return errors.New(\"tagged node missing tags\")\n    }\n    return nil\n}","typeGuard":"null","tryCatchPattern":"if errors.Is(err, state.ErrNodeMarkedTaggedButHasNoTags) { /* set tags or clear the tagged marker */ }","preventionTips":["Always mutate tags through state.SetTags rather than direct field/DB writes","In tests, build tagged fixtures with a real tag value"],"tags":["headscale","tags","ownership","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}