juanfont/headscale · error
handling existing node: %w
Error message
handling existing node: %w
What it means
Wraps a failure of handleLogout when called on an existing node during registration (hscontrol/auth.go:103). This branch handles a RegisterRequest with Auth=nil and a non-zero Expiry for a node already known by node key; handleLogout either deletes an ephemeral node or sets its expiry to now. Any state/DB error from DeleteNode or SetNodeExpiry is re-wrapped with this message.
Source
Thrown at hscontrol/auth.go:103
// the Noise session's machine key matches the cached node.
// Without this check anyone holding a target's NodeKey could
// open a Noise session with a throwaway machine key and read
// the owner's User/Login back through [nodeToRegisterResponse].
// [Headscale.handleLogout] enforces the same check on its own path.
err := machineKeyMismatch(node, machineKey)
if err != nil {
return nil, err
}
// When tailscaled restarts, it sends [tailcfg.RegisterRequest] with Auth=nil and Expiry=zero.
// Return the current node state without modification.
if req.Expiry.IsZero() && !node.IsExpired() {
return nodeToRegisterResponse(node), nil
}
resp, err := h.handleLogout(node, req, machineKey)
if err != nil {
return nil, fmt.Errorf("handling existing node: %w", err)
}
// If resp is not nil, we have a response to return to the node.
// If resp is nil, we should proceed and see if the node is trying to re-auth.
if resp != nil {
return resp, nil
}
} else {
// If the register request is not attempting to register a node, and
// we cannot match it with an existing node, we consider that unexpected
// as only register nodes should attempt to log out.
log.Debug().
Str("node.key", req.NodeKey.ShortString()).
Str("machine.key", machineKey.ShortString()).
Bool("unexpected", true).
Msg("received register request with no auth, and no existing node")
}
}View on GitHub (pinned to 565fd254d0)
Solutions
- Read the wrapped error in logs to identify the failing operation (delete vs expiry update) and the DB-level cause.
- Stabilize the database: for SQLite avoid concurrent writers and check file permissions; for PostgreSQL check connection limits and restart policies.
- Retry the operation from the client — logout expiry updates are idempotent, a retry after DB recovery succeeds.
- If caused by ephemeral-node deletion races, upgrade headscale (state-layer locking improvements land regularly) before deep-diving.
Defensive patterns
Strategy: retry
Try / catch
resp, err := h.handleRegister(req, mk)
if err != nil && strings.Contains(err.Error(), "handling existing node") {
// logout of a known node failed at the state layer; retry after DB recovery or expire via CLI
} Prevention
- Schedule node logouts away from DB maintenance windows.
- On persistent failure, use `headscale nodes expire` as the operator-side equivalent.
- Monitor SQLite lock timeouts under node churn.
When it happens
Trigger: tailscaled sends a register request with an expiry (logout) for an existing node while the database errors: ephemeral deletion fails (FK/constraint/lock) or SetNodeExpiry fails (connection lost, SQLite lock timeout).
Common situations: Nodes logging out during DB maintenance windows; ephemeral nodes being deleted while their peers stream updates (lock contention on SQLite); DB connection pool exhaustion under load.
Related errors
- handling logout: %w
- handling register with auth key: %w
- handling register interactive: %w
- deleting ephemeral node: %w
- setting node expiry: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/fd6120e84148c83e.
Report an issue: GitHub.