juanfont/headscale · critical
saving user: %w
Error message
saving user: %w
What it means
Migration 202505141324 failed saving a user after applying CleanIdentifier to the provider identifier. tx.Save(user) rewrites the full row, so failure can be the save itself (lock, connection) or a constraint violation: the unique partial index on provider_identifier rejects the cleaned value because another user already owns it.
Source
Thrown at hscontrol/db/db.go:233
},
Rollback: func(db *gorm.DB) error { return nil },
},
// Fix the provider identifier for users that have a double slash in the
// provider identifier.
{
ID: "202505141324",
Migrate: func(tx *gorm.DB) error {
users, err := ListUsers(tx, nil)
if err != nil {
return fmt.Errorf("listing users: %w", err)
}
for _, user := range users {
user.ProviderIdentifier.String = types.CleanIdentifier(user.ProviderIdentifier.String)
err := tx.Save(user).Error
if err != nil {
return fmt.Errorf("saving user: %w", err)
}
}
return nil
},
Rollback: func(db *gorm.DB) error { return nil },
},
// v0.27.0
// Schema migration to ensure all tables match the expected schema.
// This migration recreates all tables to match the exact structure in schema.sql,
// preserving all data during the process.
// Only SQLite will be migrated for consistency.
{
ID: "202507021200",
Migrate: func(tx *gorm.DB) error {
// Only run on SQLite
if cfg.Database.Type != types.DatabaseSqlite {
log.Info().Msg("skipping schema migration on non-SQLite database")View on GitHub (pinned to 565fd254d0)
Solutions
- Check the wrapped error for a unique-constraint violation on provider_identifier.
- If duplicate users result from the cleanup, merge or delete the redundant user in the source IdP / database, then re-run.
- For transient errors, retry the migration - each Save is independent within the transaction.
- Always back up the DB before upgrading past v0.25 so this migration can be re-attempted.
Defensive patterns
Strategy: try-catch
Validate before calling
-- Pre-upgrade check on a backup copy: will cleaning collide? SELECT provider_identifier, COUNT(*) FROM users WHERE provider_identifier LIKE '%//%' GROUP BY provider_identifier;
Try / catch
if _, err := db.NewHeadscaleDatabase(cfg); err != nil {
if strings.Contains(err.Error(), "saving user") && strings.Contains(err.Error(), "UNIQUE") {
// identifier collision after cleaning: restore backup, merge duplicate IdP accounts, retry
}
} Prevention
- Deduplicate OIDC accounts that differ only by duplicate slashes before upgrading.
- Always snapshot the database before migrations that rewrite user rows.
- Watch server logs during first startup after upgrade; intervene on constraint errors.
When it happens
Trigger: Two users whose identifiers differ only by duplicate slashes (e.g. 'https://idp//a' and 'https://idp/a') - cleaning both produces identical values, violating idx_provider_identifier. Also plain connection/lock failures mid-loop.
Common situations: OIDC providers that changed their identifier format over time, creating near-duplicate accounts that collapse after cleaning.
Related errors
- listing users: %w
- copying data: %w
- creating index: %w
- foreign key constraints violated
- version check: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/02307f427c741286.
Report an issue: GitHub.