juanfont/headscale · error
handling logout: %w
Error message
handling logout: %w
What it means
Wraps a failure of Headscale.handleLogout during the interactive registration path in handleRegister (hscontrol/auth.go:63). This branch runs when a RegisterRequest arrives with Expiry in the past — tailscaled's logout flow — and a node matching req.NodeKey exists in the NodeStore. The underlying error comes from either DeleteNode (ephemeral node teardown) or SetNodeExpiry inside handleLogout.
Source
Thrown at hscontrol/auth.go:63
// A past expiry takes precedence - it's a logout regardless of other fields.
if !req.Expiry.IsZero() && req.Expiry.Before(time.Now()) {
log.Debug().
Str("node.key", req.NodeKey.ShortString()).
Time("expiry", req.Expiry).
Bool("has_auth", req.Auth != nil).
Msg("Detected logout attempt with past expiry")
// This is a logout attempt (expiry in the past)
if node, ok := h.state.GetNodeByNodeKey(req.NodeKey); ok {
log.Debug().
EmbedObject(node).
Bool("is_ephemeral", node.IsEphemeral()).
Bool("has_authkey", node.AuthKey().Valid()).
Msg("Found existing node for logout, calling handleLogout")
resp, err := h.handleLogout(node, req, machineKey)
if err != nil {
return nil, fmt.Errorf("handling logout: %w", err)
}
if resp != nil {
return resp, nil
}
} else {
log.Warn().
Str("node.key", req.NodeKey.ShortString()).
Msg("Logout attempt but node not found in NodeStore")
}
}
// If the register request does not contain a Auth struct, it means we are logging
// out an existing node (legacy logout path for clients that send Auth=nil).
if req.Auth == nil {
// If the register request present a NodeKey that is currently in use, we will
// check if the node needs to be sent to re-auth, or if the node is logging out.
// We do not look up nodes by [key.MachinePublic] as it might belong to multipleView on GitHub (pinned to 565fd254d0)
Solutions
- Check headscale logs for the underlying wrapped error (locking, connection refused, constraint) — the DB error identifies the real cause.
- For SQLite 'database is locked', reduce concurrent writes, ensure only one headscale writes the DB file, and enable WAL mode as shipped.
- Verify database connectivity/health (postgres: pg_isready; service status) and retry the logout from the client afterwards.
- If it persists, capture logs and DB state and report upstream; logout paths should be retryable by re-running `tailscale logout` on the node.
Defensive patterns
Strategy: retry
Try / catch
resp, err := h.handleRegister(req, mk)
if err != nil && strings.Contains(err.Error(), "handling logout") {
// DB-layer failure mid-logout: safe to have the client retry `tailscale logout` after DB health returns
} Prevention
- Keep database health solid (connection limits, single writer for SQLite, WAL enabled).
- Treat logout as retryable: instruct the node to re-run logout on transient errors.
- Alert on wrapped DB errors inside registration paths to catch degradation early.
When it happens
Trigger: tailscale logout / tailscale switch on a registered node while the database is failing (SQLite locked by a concurrent writer, PostgreSQL unreachable); ephemeral node logout hitting a DB constraint or FK error during DeleteNode; state-layer consistency errors when marking expiry.
Common situations: SQLite deployments with concurrent registrations and long transactions causing 'database is locked'; DB restart or network blip exactly when a user logs out a node; multi-replica setups pointed at a database with exhausted connections.
Related errors
- handling existing node: %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/4714fa24f6c189ac.
Report an issue: GitHub.