juanfont/headscale · critical
automigrating types.Route: %w
Error message
automigrating types.Route: %w
What it means
Migration-time error: tx.AutoMigrate(&types.Route{}) failed while cleaning up the legacy routes table (the migration first deletes rows with null node_id, then re-aligns the table with the Go struct). Route is deprecated and kept only for migrations, so failure is almost always environmental or schema-drift, not application logic.
Source
Thrown at hscontrol/db/db.go:97
// Remove any invalid routes associated with a node that does not exist.
if tx.Migrator().HasTable(&types.Route{}) && tx.Migrator().HasTable(&types.Node{}) { //nolint:staticcheck // SA1019: Route kept for migrations
err := tx.Exec("delete from routes where node_id not in (select id from nodes)").Error
if err != nil {
return err
}
}
// Remove any invalid routes without a node_id.
if tx.Migrator().HasTable(&types.Route{}) { //nolint:staticcheck // SA1019: Route kept for migrations
err := tx.Exec("delete from routes where node_id is null").Error
if err != nil {
return err
}
}
err := tx.AutoMigrate(&types.Route{}) //nolint:staticcheck // SA1019: Route kept for migrations
if err != nil {
return fmt.Errorf("automigrating types.Route: %w", err)
}
return nil
},
Rollback: func(db *gorm.DB) error { return nil },
},
// Add back constraint so you cannot delete preauth keys that
// is still used by a node.
{
ID: "202501311657",
Migrate: func(tx *gorm.DB) error {
err := tx.AutoMigrate(&types.PreAuthKey{})
if err != nil {
return fmt.Errorf("automigrating types.PreAuthKey: %w", err)
}
err = tx.AutoMigrate(&types.Node{})
if err != nil {View on GitHub (pinned to 565fd254d0)
Solutions
- Check the wrapped driver error for the exact DDL failure (permission, lock, type mismatch).
- Back up the database, then inspect the routes table schema and reconcile it with types.Route.
- Ensure exclusive access during startup migrations (stop other headscale instances).
- Restore from backup and upgrade stepwise through supported versions if schema drift is severe.
Defensive patterns
Strategy: try-catch
Try / catch
hsdb, err := db.NewHeadscaleDatabase(cfg)
if err != nil {
// migrations are transactional; fix the environment (locks/permissions/disk) and restart
log.Fatal().Err(err).Msg("migration failed; database unchanged - resolve the wrapped error and restart")
} Prevention
- Take a file-level backup before upgrading (SQLite: copy the .db while headscale is stopped).
- Give the migration role full ALTER/DDL privileges on the database.
- Ensure exclusive database access during startup; stop old instances first.
When it happens
Trigger: Upgrading a database where the routes table has an incompatible structure (columns with wrong types, indexes that block DDL), the DB user lacks ALTER privileges, or SQLite file is locked/read-only.
Common situations: Upgrading from an old/forked headscale whose routes schema differs; running migrations on a read-only SQLite file; Postgres user missing ALTER TABLE rights.
Related errors
- adding prefix column: %w
- automigrating types.Node: %w
- checking if table %s exists: %w
- adding hash column: %w
- clearing expiry on tagged nodes: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/c08e1751acf42cf5.
Report an issue: GitHub.