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

  1. Inspect the chained error (err.Error()/errors.As) — the real cause is the wrapped %w, not this message
  2. Retry the rename once: transient copy-on-write swap failures under concurrent updates often clear
  3. Check server logs for NodeStore consistency errors around the same timestamp
  4. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/23c1fdb808ff11e0. Report an issue: GitHub.