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

  1. Note whether the wrapped error is transient (lock/timeout) - retry usually succeeds.
  2. Verify the approved_routes column was created as a text/jsonb-compatible type by the earlier step.
  3. Back up and prune absurdly large route sets in the routes table before migrating.
  4. 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

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


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