juanfont/headscale · error
renaming node: %w
Error message
renaming node: %w
What it means
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.
Source
Thrown at hscontrol/state/state.go:1058
// bumping a user-facing label. See HOSTNAME.md for the CLI contract.
func (s *State) RenameNode(nodeID types.NodeID, newName string) (types.NodeView, change.Change, error) {
// Validate the label AND that the resulting FQDN fits MaxHostnameLength:
// a valid 63-char label can still overflow under a long base_domain, and
// an unmappable name would break this node and its peers (issue #3346).
err := types.ValidateGivenName(newName, s.cfg.BaseDomain)
if err != nil {
return types.NodeView{}, change.Change{}, fmt.Errorf("%w: %w", ErrGivenNameInvalid, err)
}
view, err := s.nodeStore.SetGivenName(nodeID, newName)
if err != nil {
switch {
case errors.Is(err, ErrGivenNameTaken):
return types.NodeView{}, change.Change{}, fmt.Errorf("%w: %s", ErrNodeNameNotUnique, newName)
case errors.Is(err, ErrNodeNotFound):
return types.NodeView{}, change.Change{}, fmt.Errorf("%w: %d", ErrNodeNotInNodeStore, nodeID)
default:
return types.NodeView{}, change.Change{}, fmt.Errorf("renaming node: %w", err)
}
}
return s.persistNodeToDB(view)
}
// BackfillNodeIPs assigns IP addresses to nodes that don't have them.
func (s *State) BackfillNodeIPs() ([]string, error) {
changes, err := s.db.BackfillNodeIPs(s.ipAlloc)
if err != nil {
return nil, err
}
// Refresh [NodeStore] after IP changes to ensure consistency
if len(changes) > 0 {
nodes, err := s.db.ListNodes()
if err != nil {
return changes, fmt.Errorf("refreshing NodeStore after IP backfill: %w", err)View on GitHub (pinned to 565fd254d0)
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
Example fix
// before
view, ch, err := h.state.RenameNode(id, name)
if err != nil {
return err // opaque
}
// after
view, ch, err := h.state.RenameNode(id, name)
if err != nil {
if errors.Is(err, state.ErrNodeNameNotUnique) || errors.Is(err, state.ErrNodeNotInNodeStore) {
return err // user-facing, actionable
}
log.Error().Err(err).Uint64("id", id).Msg("unexpected rename failure")
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
// Guard the two user-correctable cases before calling:
if _, exists := store.GetNode(nodeID); !exists {
return state.ErrNodeNotInNodeStore
}
if existing, _ := store.GetGivenNameByName(newName); existing.Valid() && existing.ID() != nodeID {
return state.ErrNodeNameNotUnique
} Type guard
func isNodeStoreInternalErr(err error) bool {
return err != nil &&
!errors.Is(err, state.ErrNodeNameNotUnique) &&
!errors.Is(err, state.ErrNodeNotInNodeStore) &&
!errors.Is(err, state.ErrGivenNameInvalid)
} Try / catch
view, ch, err := h.state.RenameNode(id, name)
if err != nil {
switch {
case errors.Is(err, state.ErrNodeNameNotUnique):
return status.Errorf(codes.AlreadyExists, "name %q taken", name)
case errors.Is(err, state.ErrNodeNotInNodeStore), errors.Is(err, state.ErrNodeNotFound):
return status.Errorf(codes.NotFound, "node %d not found", id)
default:
log.Error().Err(err).Msg("rename: unexpected NodeStore failure")
return status.Error(codes.Internal, "internal error")
}
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- given name already in use by another node
- refreshing NodeStore after IP backfill: %w
- MOCKOIDC_CLIENT_ID not defined
- missing parameters
- failed to parse ApiKey
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/23c1fdb808ff11e0.
Report an issue: GitHub.