juanfont/headscale · error · ErrNodeNotInNodeStore

node no longer exists in NodeStore

Error message

node no longer exists in NodeStore

What it means

ErrNodeNotInNodeStore is a sentinel error in hscontrol/state/state.go:75 indicating that a node being updated no longer exists in the in-memory copy-on-write NodeStore snapshot. Write paths wrap it with the node ID (state.go:543, 912, 983, 1018, 3182). It differs from ErrNodeNotFound: the node vanished from the live store during an in-flight operation, typically because it was deleted concurrently.

Source

Thrown at hscontrol/state/state.go:75

	// before rebuilding the in-memory node snapshot.
	defaultNodeStoreBatchSize = 100

	// defaultNodeStoreBatchTimeout is the default maximum time to wait before
	// processing a partial batch of node operations.
	defaultNodeStoreBatchTimeout = 500 * time.Millisecond
)

// ErrUnsupportedPolicyMode is returned for invalid policy modes. Valid modes are "file" and "db".
var ErrUnsupportedPolicyMode = errors.New("unsupported policy mode")

// ErrNodeNotFound is returned when a node cannot be found by its ID.
var ErrNodeNotFound = errors.New("node not found")

// ErrInvalidNodeView is returned when an invalid node view is provided.
var ErrInvalidNodeView = errors.New("invalid node view provided")

// ErrNodeNotInNodeStore is returned when a node no longer exists in the [NodeStore].
var ErrNodeNotInNodeStore = errors.New("node no longer exists in NodeStore")

// ErrNodeNameNotUnique is returned when a node name is not unique.
var ErrNodeNameNotUnique = errors.New("node name is not unique")

// nodeUpdateColumns lists all Node columns that should be written
// during a struct-based GORM Updates() call.  Listing them explicitly
// forces GORM to include nil/zero-value fields (e.g. UserID=nil when
// converting a user-owned node to tagged) that struct-based Updates()
// would otherwise silently skip.
//
// Excluded columns:
//   - AuthKeyID, AuthKey: prevents GORM from persisting stale
//     PreAuthKey references after a key has been deleted (#2862).
//   - User: GORM association, not a real column.
//   - IsOnline: runtime-only field (gorm:"-").
//
// Expiry is included here but may be omitted at call sites that must
// not touch it (see persistNodeToDB).

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Treat as a stale-reference condition: re-fetch the node by ID and abort the mutation if absent
  2. If the node should still exist, check deletion/expiry logs to find who removed it
  3. In concurrent admin tooling, serialize delete + update operations per node ID
  4. Map to 404 in API handlers rather than 500 (v1 errors.go already maps it alongside ErrNodeNotFound)

Example fix

null
Defensive patterns

Strategy: try-catch

Validate before calling

null

Type guard

null

Try / catch

if errors.Is(err, state.ErrNodeNotInNodeStore) {
    // node vanished mid-operation: re-fetch; if gone, abandon the update
}

Prevention

When it happens

Trigger: UpdateNodeFromMapRequest, SetTags, SetGivenName, or approve-route operations racing with a node deletion; a MapRequest arriving from a node that was just deleted; batched NodeStore writes discovering the target node's snapshot entry is gone.

Common situations: Admin deletes/expiry-reaps a node at the same moment the node sends a MapRequest or an API call mutates it; test suites tearing down nodes while async mappers still stream updates.

Related errors


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