{"record":{"id":"af9f773d834c0b02","repo":"juanfont/headscale","slug":"node-has-neither-user-nor-tags-must-be-owned-by","errorCode":null,"errorMessage":"node has neither user nor tags - must be owned by user or tagged","messagePattern":"node has neither user nor tags - must be owned by user or tagged","errorType":"http","errorClass":"ErrNodeHasNeitherUserNorTags","httpStatus":400,"severity":"error","filePath":"hscontrol/state/tags.go","lineNumber":16,"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() {\n\t\tif len(node.Tags) == 0 {\n\t\t\treturn fmt.Errorf(\"%w: %q\", ErrNodeMarkedTaggedButHasNoTags, node.Hostname)\n\t\t}","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/state/tags.go#L1-L34","documentation":"ErrNodeHasNeitherUserNorTags is a sentinel error in hscontrol/state/tags.go:16 returned by validateNodeOwnership when a node has neither a UserID nor any tags. Headscale's ownership model is strict: every node must be either user-owned or tagged, so an ownerless node is rejected.","triggerScenarios":"Creating a types.Node with no user and no tags via internal APIs; deleting a node's user without reassigning ownership; registration flows leaving ownership unset before persistence; test fixtures forgetting to set user or tags.","commonSituations":"User deletion flows that orphan nodes; direct DB manipulation or imports creating ownerless rows; code paths converting user-owned to tagged but clearing the user before tags land.","solutions":["Assign a user or at least one tag to the node before persisting it","When deleting users, delete or reassign their nodes first (db DestroyUser scope handles pre-auth keys; check node handling)","Route node creation through the registration flow so ownership is always set","Repair existing ownerless rows in the DB by setting user_id or tags"],"exampleFix":"null","handlingStrategy":"validation","validationCode":"func hasOwner(n types.NodeView) bool {\n    return n.IsTagged() || n.UserID().Valid()\n}","typeGuard":"null","tryCatchPattern":"if errors.Is(err, state.ErrNodeHasNeitherUserNorTags) { /* assign user or tags before persisting */ }","preventionTips":["When deleting users, delete or reassign their nodes first","Never persist ownerless Node structs; go through registration flows"],"tags":["headscale","ownership","validation","data-integrity"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}