juanfont/headscale · critical
saving approved routes to new column: %w
Error message
saving approved routes to new column: %w
What it means
Migration 202502131714 failed writing the JSON-encoded approved_routes array into a node row. The json.Marshal result is written via UPDATE nodes ... SET approved_routes = ?. Failures are per-row: connection loss, lock timeout, or a column type/constraint mismatch added moments earlier in the same migration.
Source
Thrown at hscontrol/db/db.go:181
if err != nil {
return fmt.Errorf("fetching routes: %w", err)
}
for _, route := range routes {
if route.Enabled {
nodeRoutes[route.NodeID] = append(nodeRoutes[route.NodeID], route.Prefix)
}
}
for nodeID, routes := range nodeRoutes {
slices.SortFunc(routes, netip.Prefix.Compare)
routes = slices.Compact(routes)
data, _ := json.Marshal(routes)
err = tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("approved_routes", data).Error
if err != nil {
return fmt.Errorf("saving approved routes to new column: %w", err)
}
}
// Drop the old table.
_ = tx.Migrator().DropTable(&types.Route{}) //nolint:staticcheck // SA1019: Route kept for migrations
return nil
},
Rollback: func(db *gorm.DB) error { return nil },
},
{
ID: "202502171819",
Migrate: func(tx *gorm.DB) error {
// This migration originally removed the last_seen column
// from the node table, but it was added back in
// 202505091439.
return nil
},View on GitHub (pinned to 565fd254d0)
Solutions
- Note whether the wrapped error is transient (lock/timeout) - retry usually succeeds.
- Verify the approved_routes column was created as a text/jsonb-compatible type by the earlier step.
- Back up and prune absurdly large route sets in the routes table before migrating.
- Restore from backup and rerun after fixing connectivity/locks.
Defensive patterns
Strategy: try-catch
Try / catch
if _, err := db.NewHeadscaleDatabase(cfg); err != nil {
if strings.Contains(err.Error(), "saving approved routes") {
// per-row UPDATE failed; migration transaction rolled back - fix env and retry
}
} Prevention
- Review and disable obsolete subnet route approvals before upgrading.
- Verify approved_routes column compatibility if the schema was touched by external tools.
- Back up the database before the v0.26 upgrade.
When it happens
Trigger: UPDATE on a specific node id rejected due to lock, disconnection, or (on forked schemas) approved_routes existing with an incompatible type; very large route sets producing oversized JSON on strict column size limits.
Common situations: Nodes with hundreds of approved subnet routes; connection reset during a long migration loop; schema drift from third-party builds.
Related errors
- adding column types.Node: %w
- fetching routes: %w
- serializing merged tags for node %d: %w
- foreign key constraints violated
- auto approving routes: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/05e12da9466d529b.
Report an issue: GitHub.