juanfont/headscale · critical

creating prefix index: %w

Error message

creating prefix index: %w

What it means

Migration '202511011637-preauthkey-bcrypt' fails to create the partial unique index idx_pre_auth_keys_prefix on pre_auth_keys(prefix). The most common cause is existing duplicate non-empty prefix values violating uniqueness; other causes are missing CREATE INDEX privilege, lock contention, or an index with the same name on a different table. IF NOT EXISTS protects only against the index already existing, not against constraint violations.

Source

Thrown at hscontrol/db/db.go:537

						err := tx.Migrator().AddColumn(&types.PreAuthKey{}, "prefix")
						if err != nil {
							return fmt.Errorf("adding prefix column: %w", err)
						}
					}

					// Check and add hash column if it doesn't exist
					if !tx.Migrator().HasColumn(&types.PreAuthKey{}, "hash") {
						err := tx.Migrator().AddColumn(&types.PreAuthKey{}, "hash")
						if err != nil {
							return fmt.Errorf("adding hash column: %w", err)
						}
					}

					// Create partial unique index to allow multiple legacy keys (NULL/empty prefix)
					// while enforcing uniqueness for new bcrypt-based keys
					err := tx.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_pre_auth_keys_prefix ON pre_auth_keys(prefix) WHERE prefix IS NOT NULL AND prefix != ''").Error
					if err != nil {
						return fmt.Errorf("creating prefix index: %w", err)
					}

					return nil
				},
				Rollback: func(db *gorm.DB) error { return nil },
			},
			{
				ID: "202511122344-remove-newline-index",
				Migrate: func(tx *gorm.DB) error {
					// Reformat multi-line indexes to single-line for consistency
					// This migration drops and recreates the three user identity indexes
					// to match the single-line format expected by schema validation

					// Drop existing multi-line indexes
					dropIndexes := []string{
						`DROP INDEX IF EXISTS idx_provider_identifier`,
						`DROP INDEX IF EXISTS idx_name_provider_identifier`,
						`DROP INDEX IF EXISTS idx_name_no_provider_identifier`,

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Check the wrapped error - if it reports a duplicate key value, find offending rows: SELECT prefix, count(*) FROM pre_auth_keys WHERE prefix IS NOT NULL AND prefix != '' GROUP BY prefix HAVING count(*) > 1
  2. Deduplicate: keep the newest key per prefix and clear prefix/hash on the obsolete duplicates (or delete them) via a manual SQL session, then restart headscale
  3. If the cause is permissions/locking instead, grant CREATE INDEX or serialize access as with the AddColumn errors
  4. Restore from a pre-upgrade backup and re-run the migration cleanly if the data was hand-modified

Example fix

-- before: duplicates block the unique index
SELECT prefix, count(*) FROM pre_auth_keys WHERE prefix IS NOT NULL AND prefix != '' GROUP BY prefix HAVING count(*) > 1;
-- after: neutralize duplicates, then restart headscale
UPDATE pre_auth_keys SET prefix = NULL, hash = NULL WHERE id NOT IN (SELECT max(id) FROM pre_auth_keys WHERE prefix IS NOT NULL GROUP BY prefix);
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight: would the partial unique index succeed?
rows, err := db.Query(`SELECT prefix FROM pre_auth_keys
	WHERE prefix IS NOT NULL AND prefix != ''
	GROUP BY prefix HAVING count(*) > 1`)
if err != nil { return err }
if rows.Next() {
	return errors.New("duplicate pre_auth_keys.prefix values would violate the unique index")
}

Prevention

When it happens

Trigger: Legacy pre_auth_keys rows that already contain a prefix column (e.g. from a partially applied earlier attempt) with duplicated prefix strings; CREATE UNIQUE INDEX ... WHERE prefix IS NOT NULL AND prefix != '' then aborts with a unique-constraint violation from the database.

Common situations: Re-running an upgrade after a crashed earlier migration attempt that added prefixes but not the index; hand-edited databases where prefixes were copied between keys; DB user without INDEX privilege on PostgreSQL.

Related errors


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