{"record":{"id":"05e12da9466d529b","repo":"juanfont/headscale","slug":"saving-approved-routes-to-new-column-w","errorCode":null,"errorMessage":"saving approved routes to new column: %w","messagePattern":"saving approved routes to new column: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/db/db.go","lineNumber":181,"sourceCode":"\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"fetching routes: %w\", err)\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, route := range routes {\n\t\t\t\t\t\tif route.Enabled {\n\t\t\t\t\t\t\tnodeRoutes[route.NodeID] = append(nodeRoutes[route.NodeID], route.Prefix)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tfor nodeID, routes := range nodeRoutes {\n\t\t\t\t\t\tslices.SortFunc(routes, netip.Prefix.Compare)\n\t\t\t\t\t\troutes = slices.Compact(routes)\n\n\t\t\t\t\t\tdata, _ := json.Marshal(routes)\n\n\t\t\t\t\t\terr = tx.Model(&types.Node{}).Where(\"id = ?\", nodeID).Update(\"approved_routes\", data).Error\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"saving approved routes to new column: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Drop the old table.\n\t\t\t\t\t_ = tx.Migrator().DropTable(&types.Route{}) //nolint:staticcheck // SA1019: Route kept for migrations\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tRollback: func(db *gorm.DB) error { return nil },\n\t\t\t},\n\t\t\t{\n\t\t\t\tID: \"202502171819\",\n\t\t\t\tMigrate: func(tx *gorm.DB) error {\n\t\t\t\t\t// This migration originally removed the last_seen column\n\t\t\t\t\t// from the node table, but it was added back in\n\t\t\t\t\t// 202505091439.\n\t\t\t\t\treturn nil\n\t\t\t\t},","sourceCodeStart":163,"sourceCodeEnd":199,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/db.go#L163-L199","documentation":"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.","triggerScenarios":"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.","commonSituations":"Nodes with hundreds of approved subnet routes; connection reset during a long migration loop; schema drift from third-party builds.","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."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if _, err := db.NewHeadscaleDatabase(cfg); err != nil {\n    if strings.Contains(err.Error(), \"saving approved routes\") {\n        // per-row UPDATE failed; migration transaction rolled back - fix env and retry\n    }\n}","preventionTips":["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."],"tags":["database","migration","routes","json","update"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}