{"record":{"id":"23c1fdb808ff11e0","repo":"juanfont/headscale","slug":"renaming-node-w-23c1fd","errorCode":null,"errorMessage":"renaming node: %w","messagePattern":"renaming node: %w","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"hscontrol/state/state.go","lineNumber":1058,"sourceCode":"// bumping a user-facing label. See HOSTNAME.md for the CLI contract.\nfunc (s *State) RenameNode(nodeID types.NodeID, newName string) (types.NodeView, change.Change, error) {\n\t// Validate the label AND that the resulting FQDN fits MaxHostnameLength:\n\t// a valid 63-char label can still overflow under a long base_domain, and\n\t// an unmappable name would break this node and its peers (issue #3346).\n\terr := types.ValidateGivenName(newName, s.cfg.BaseDomain)\n\tif err != nil {\n\t\treturn types.NodeView{}, change.Change{}, fmt.Errorf(\"%w: %w\", ErrGivenNameInvalid, err)\n\t}\n\n\tview, err := s.nodeStore.SetGivenName(nodeID, newName)\n\tif err != nil {\n\t\tswitch {\n\t\tcase errors.Is(err, ErrGivenNameTaken):\n\t\t\treturn types.NodeView{}, change.Change{}, fmt.Errorf(\"%w: %s\", ErrNodeNameNotUnique, newName)\n\t\tcase errors.Is(err, ErrNodeNotFound):\n\t\t\treturn types.NodeView{}, change.Change{}, fmt.Errorf(\"%w: %d\", ErrNodeNotInNodeStore, nodeID)\n\t\tdefault:\n\t\t\treturn types.NodeView{}, change.Change{}, fmt.Errorf(\"renaming node: %w\", err)\n\t\t}\n\t}\n\n\treturn s.persistNodeToDB(view)\n}\n\n// BackfillNodeIPs assigns IP addresses to nodes that don't have them.\nfunc (s *State) BackfillNodeIPs() ([]string, error) {\n\tchanges, err := s.db.BackfillNodeIPs(s.ipAlloc)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Refresh [NodeStore] after IP changes to ensure consistency\n\tif len(changes) > 0 {\n\t\tnodes, err := s.db.ListNodes()\n\t\tif err != nil {\n\t\t\treturn changes, fmt.Errorf(\"refreshing NodeStore after IP backfill: %w\", err)","sourceCodeStart":1040,"sourceCodeEnd":1076,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/state/state.go#L1040-L1076","documentation":"Generic wrapper raised by State.RenameNode when NodeStore.SetGivenName fails with an error that is neither ErrGivenNameTaken nor ErrNodeNotFound. It signals an unexpected failure inside the copy-on-write NodeStore during a rename, after the specific name-collision and missing-node cases were already classified. The underlying error is chained with %w so errors.Is/As can inspect it.","triggerScenarios":"Calling RenameNode(nodeID, newName) on a valid node with a unique, DNS-valid name while the NodeStore snapshot swap fails internally (e.g. write-batch machinery error, memory pressure, or an internal invariant violation in node_store.go that is not one of the three documented sentinel cases).","commonSituations":"Administrators renaming nodes via the gRPC API (`headscale nodes rename`) on a large tailnet where the snapshot rebuild races with heavy concurrent writes, or after a version upgrade that changed NodeStore internals while old in-process state lingers.","solutions":["Inspect the chained error (err.Error()/errors.As) — the real cause is the wrapped %w, not this message","Retry the rename once: transient copy-on-write swap failures under concurrent updates often clear","Check server logs for NodeStore consistency errors around the same timestamp","If reproducible, capture the wrapped error text and file an issue; this branch should be unreachable in normal operation"],"exampleFix":"// before\nview, ch, err := h.state.RenameNode(id, name)\nif err != nil {\n    return err // opaque\n}\n\n// after\nview, ch, err := h.state.RenameNode(id, name)\nif err != nil {\n    if errors.Is(err, state.ErrNodeNameNotUnique) || errors.Is(err, state.ErrNodeNotInNodeStore) {\n        return err // user-facing, actionable\n    }\n    log.Error().Err(err).Uint64(\"id\", id).Msg(\"unexpected rename failure\")\n    return err\n}","handlingStrategy":"try-catch","validationCode":"// Guard the two user-correctable cases before calling:\nif _, exists := store.GetNode(nodeID); !exists {\n    return state.ErrNodeNotInNodeStore\n}\nif existing, _ := store.GetGivenNameByName(newName); existing.Valid() && existing.ID() != nodeID {\n    return state.ErrNodeNameNotUnique\n}","typeGuard":"func isNodeStoreInternalErr(err error) bool {\n    return err != nil &&\n        !errors.Is(err, state.ErrNodeNameNotUnique) &&\n        !errors.Is(err, state.ErrNodeNotInNodeStore) &&\n        !errors.Is(err, state.ErrGivenNameInvalid)\n}","tryCatchPattern":"view, ch, err := h.state.RenameNode(id, name)\nif err != nil {\n    switch {\n    case errors.Is(err, state.ErrNodeNameNotUnique):\n        return status.Errorf(codes.AlreadyExists, \"name %q taken\", name)\n    case errors.Is(err, state.ErrNodeNotInNodeStore), errors.Is(err, state.ErrNodeNotFound):\n        return status.Errorf(codes.NotFound, \"node %d not found\", id)\n    default:\n        log.Error().Err(err).Msg(\"rename: unexpected NodeStore failure\")\n        return status.Error(codes.Internal, \"internal error\")\n    }\n}","preventionTips":["Always classify RenameNode errors with errors.Is against the three sentinels before treating the rest as internal","Log the wrapped chain — the default branch carries the real NodeStore cause via %w","For admin tooling, pre-check name uniqueness and DNS-label validity to surface friendly errors"],"tags":["state","node-store","rename","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}