{"record":{"id":"3dcf53e98458d3b8","repo":"juanfont/headscale","slug":"creating-oauth-access-tokens-index-w","errorCode":null,"errorMessage":"creating oauth_access_tokens index: %w","messagePattern":"creating oauth_access_tokens index: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/db/db.go","lineNumber":895,"sourceCode":"\n\t\t\t\t\tif !tx.Migrator().HasTable(&types.OAuthAccessToken{}) {\n\t\t\t\t\t\terr := tx.Exec(`CREATE TABLE oauth_access_tokens(\n  id integer PRIMARY KEY AUTOINCREMENT,\n  prefix text,\n  hash blob,\n  client_id text,\n  scopes text,\n  tags text,\n  expiration datetime,\n  created_at datetime\n)`).Error\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"creating oauth_access_tokens table: %w\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\terr = tx.Exec(`CREATE UNIQUE INDEX idx_oauth_access_tokens_prefix ON oauth_access_tokens(prefix)`).Error\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"creating oauth_access_tokens index: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tRollback: func(db *gorm.DB) error { return nil },\n\t\t\t},\n\t\t\t{\n\t\t\t\t// Clear stale key expiry on tagged nodes. A tagged node is\n\t\t\t\t// owned by its tags and never expires (KB 1068), but a buggy\n\t\t\t\t// handleLogout stamped a past expiry on it, leaving it\n\t\t\t\t// permanently Expired and unable to re-authenticate. The\n\t\t\t\t// buggy writer is fixed, so this only repairs rows written\n\t\t\t\t// before the upgrade; a fixed server cannot recreate them.\n\t\t\t\t// Match the tagged-node predicate the earlier\n\t\t\t\t// clear-tagged-node-user-id migration uses (a nil tags slice\n\t\t\t\t// marshals to 'null', so exclude it).\n\t\t\t\t// Fixes: https://github.com/juanfont/headscale/issues/3371","sourceCodeStart":877,"sourceCodeEnd":913,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/db.go#L877-L913","documentation":"The oauth migration fails creating unique index idx_oauth_access_tokens_prefix on oauth_access_tokens(prefix). The table is freshly created in the same transaction so duplicates cannot exist; realistic causes are missing INDEX privilege, an aborted Postgres transaction from an earlier statement, or lock failure. Like its oauth_clients twin, this error is frequently the visible tail of an earlier failure in the same migration.","triggerScenarios":"Executing CREATE UNIQUE INDEX after a prior statement in the migration transaction already failed (Postgres abort semantics), or with CREATE INDEX revoked / database locked.","commonSituations":"Diagnosing the last line of a failed migration instead of the first; constrained roles; concurrent SQLite writers.","solutions":["Find the first error in the same migration in the log and fix that; this index error usually disappears with it","Grant the role CREATE INDEX (Postgres) or otherwise clear the wrapped permission error","Restart headscale after the fix - the guards make the migration idempotent","Verify: query pg_indexes (Postgres) or sqlite_master (SQLite) for idx_oauth_access_tokens_prefix"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Postgres: verify index-creation privilege ahead of the upgrade\nvar can bool\ndb.QueryRow(\"SELECT has_schema_privilege(current_user, current_schema(), 'CREATE')\").Scan(&can)\nif !can {\n\tlog.Fatal(\"role cannot create indexes in this schema\")\n}","typeGuard":null,"tryCatchPattern":"// Treat as symptom: on Postgres this usually means the migration transaction was\n// already aborted; catch, unwrap fully, and surface the first failure\nif err := runMigrations(db); err != nil {\n\tfor e := err; e != nil; e = errors.Unwrap(e) {\n\t\tlog.Error().Err(e).Msg(\"migration error chain\")\n\t}\n\tos.Exit(1)\n}","preventionTips":["Diagnose the first error in the migration transaction, not the last","Grant CREATE INDEX; avoid concurrent DDL from other migration tools against the same DB","Post-upgrade, verify the unique index exists: query pg_indexes / sqlite_master for idx_oauth_access_tokens_prefix"],"tags":["database","migration","oauth","unique-index","transaction-aborted"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}