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 multiple

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Check headscale logs for the underlying wrapped error (locking, connection refused, constraint) — the DB error identifies the real cause.
  2. For SQLite 'database is locked', reduce concurrent writes, ensure only one headscale writes the DB file, and enable WAL mode as shipped.
  3. Verify database connectivity/health (postgres: pg_isready; service status) and retry the logout from the client afterwards.
  4. 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

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


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