{"record":{"id":"8d7af664d00abd45","repo":"juanfont/headscale","slug":"creating-oauth-clients-index-w","errorCode":null,"errorMessage":"creating oauth_clients index: %w","messagePattern":"creating oauth_clients index: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/db/db.go","lineNumber":874,"sourceCode":"\t\t\t\t\tif !tx.Migrator().HasTable(&types.OAuthClient{}) {\n\t\t\t\t\t\terr := tx.Exec(`CREATE TABLE oauth_clients(\n  id integer PRIMARY KEY AUTOINCREMENT,\n  client_id text,\n  secret_hash blob,\n  scopes text,\n  tags text,\n  description text,\n  user_id integer,\n  created_at datetime,\n  revoked datetime\n)`).Error\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"creating oauth_clients 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_clients_client_id ON oauth_clients(client_id)`).Error\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"creating oauth_clients index: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\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","sourceCodeStart":856,"sourceCodeEnd":892,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/db.go#L856-L892","documentation":"The oauth_clients migration fails creating the unique index idx_oauth_clients_client_id on oauth_clients(client_id). The table was created moments before in the same transaction, so constraint violations are impossible; realistic causes are missing INDEX privilege, dialect issues, or - on Postgres - the transaction already aborted by an earlier failed statement, after which every Exec fails with 'current transaction is aborted'.","triggerScenarios":"Postgres aborting the transaction after a prior failed statement (this index error is collateral); CREATE INDEX revoked from the role; SQLite lock escalation failure.","commonSituations":"Least-privilege DB roles; diagnosing only the last error in a chain instead of the first failed statement of the migration.","solutions":["Scroll up in the startup log: on Postgres the first error in the migration transaction is the cause; this message is often collateral","Grant CREATE/INDEX privileges to the headscale DB role","Ensure single-instance access to SQLite during upgrade","After fixing the root cause, restart - the transaction replays and the HasTable/HasColumn guards keep it idempotent"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Postgres: confirm CREATE privilege before upgrading\nvar canCreate bool\ndb.QueryRow(\"SELECT has_schema_privilege(current_user, current_schema(), 'CREATE')\").Scan(&canCreate)\nif !canCreate {\n\tlog.Fatal(\"role lacks CREATE on schema; index creation will fail\")\n}","typeGuard":null,"tryCatchPattern":"// When wrapping startup: report the whole migration chain, not just the last error\nif err := runMigrations(db); err != nil {\n\tlog.Error().Err(errors.Unwrap(err)).Msg(\"root-cause error (this index error may be collateral of an earlier statement)\")\n\tos.Exit(1)\n}","preventionTips":["On Postgres always read the FIRST error in a failed migration transaction; later statements fail with 'transaction is aborted'","Grant INDEX/CREATE privileges before upgrading","Avoid connection poolers that break transaction semantics during migration"],"tags":["database","migration","oauth","unique-index","postgres"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}