{"record":{"id":"4714fa24f6c189ac","repo":"juanfont/headscale","slug":"handling-logout-w","errorCode":null,"errorMessage":"handling logout: %w","messagePattern":"handling logout: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/auth.go","lineNumber":63,"sourceCode":"\t// A past expiry takes precedence - it's a logout regardless of other fields.\n\tif !req.Expiry.IsZero() && req.Expiry.Before(time.Now()) {\n\t\tlog.Debug().\n\t\t\tStr(\"node.key\", req.NodeKey.ShortString()).\n\t\t\tTime(\"expiry\", req.Expiry).\n\t\t\tBool(\"has_auth\", req.Auth != nil).\n\t\t\tMsg(\"Detected logout attempt with past expiry\")\n\n\t\t// This is a logout attempt (expiry in the past)\n\t\tif node, ok := h.state.GetNodeByNodeKey(req.NodeKey); ok {\n\t\t\tlog.Debug().\n\t\t\t\tEmbedObject(node).\n\t\t\t\tBool(\"is_ephemeral\", node.IsEphemeral()).\n\t\t\t\tBool(\"has_authkey\", node.AuthKey().Valid()).\n\t\t\t\tMsg(\"Found existing node for logout, calling handleLogout\")\n\n\t\t\tresp, err := h.handleLogout(node, req, machineKey)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"handling logout: %w\", err)\n\t\t\t}\n\n\t\t\tif resp != nil {\n\t\t\t\treturn resp, nil\n\t\t\t}\n\t\t} else {\n\t\t\tlog.Warn().\n\t\t\t\tStr(\"node.key\", req.NodeKey.ShortString()).\n\t\t\t\tMsg(\"Logout attempt but node not found in NodeStore\")\n\t\t}\n\t}\n\n\t// If the register request does not contain a Auth struct, it means we are logging\n\t// out an existing node (legacy logout path for clients that send Auth=nil).\n\tif req.Auth == nil {\n\t\t// If the register request present a NodeKey that is currently in use, we will\n\t\t// check if the node needs to be sent to re-auth, or if the node is logging out.\n\t\t// We do not look up nodes by [key.MachinePublic] as it might belong to multiple","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/auth.go#L45-L81","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"resp, err := h.handleRegister(req, mk)\nif err != nil && strings.Contains(err.Error(), \"handling logout\") {\n    // DB-layer failure mid-logout: safe to have the client retry `tailscale logout` after DB health returns\n}","preventionTips":["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."],"tags":["authentication","logout","database","registration"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}