juanfont/headscale · critical
creating oauth_access_tokens index: %w
Error message
creating oauth_access_tokens index: %w
What it means
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.
Source
Thrown at hscontrol/db/db.go:895
if !tx.Migrator().HasTable(&types.OAuthAccessToken{}) {
err := tx.Exec(`CREATE TABLE oauth_access_tokens(
id integer PRIMARY KEY AUTOINCREMENT,
prefix text,
hash blob,
client_id text,
scopes text,
tags text,
expiration datetime,
created_at datetime
)`).Error
if err != nil {
return fmt.Errorf("creating oauth_access_tokens table: %w", err)
}
err = tx.Exec(`CREATE UNIQUE INDEX idx_oauth_access_tokens_prefix ON oauth_access_tokens(prefix)`).Error
if err != nil {
return fmt.Errorf("creating oauth_access_tokens index: %w", err)
}
}
return nil
},
Rollback: func(db *gorm.DB) error { return nil },
},
{
// Clear stale key expiry on tagged nodes. A tagged node is
// owned by its tags and never expires (KB 1068), but a buggy
// handleLogout stamped a past expiry on it, leaving it
// permanently Expired and unable to re-authenticate. The
// buggy writer is fixed, so this only repairs rows written
// before the upgrade; a fixed server cannot recreate them.
// Match the tagged-node predicate the earlier
// clear-tagged-node-user-id migration uses (a nil tags slice
// marshals to 'null', so exclude it).
// Fixes: https://github.com/juanfont/headscale/issues/3371View on GitHub (pinned to 565fd254d0)
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
Defensive patterns
Strategy: try-catch
Validate before calling
// Postgres: verify index-creation privilege ahead of the upgrade
var can bool
db.QueryRow("SELECT has_schema_privilege(current_user, current_schema(), 'CREATE')").Scan(&can)
if !can {
log.Fatal("role cannot create indexes in this schema")
} Try / catch
// Treat as symptom: on Postgres this usually means the migration transaction was
// already aborted; catch, unwrap fully, and surface the first failure
if err := runMigrations(db); err != nil {
for e := err; e != nil; e = errors.Unwrap(e) {
log.Error().Err(e).Msg("migration error chain")
}
os.Exit(1)
} Prevention
- 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
When it happens
Trigger: Executing CREATE UNIQUE INDEX after a prior statement in the migration transaction already failed (Postgres abort semantics), or with CREATE INDEX revoked / database locked.
Common situations: Diagnosing the last line of a failed migration instead of the first; constrained roles; concurrent SQLite writers.
Related errors
- creating oauth_clients index: %w
- creating prefix index: %w
- creating oauth_clients table: %w
- creating oauth_access_tokens table: %w
- foreign key constraints violated
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/3dcf53e98458d3b8.
Report an issue: GitHub.