{"record":{"id":"ec0a523f8865e4ef","repo":"juanfont/headscale","slug":"fetching-routes-w","errorCode":null,"errorMessage":"fetching routes: %w","messagePattern":"fetching routes: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/db/db.go","lineNumber":164,"sourceCode":"\t\t\t// Migrate all routes from the Route table to the new field ApprovedRoutes\n\t\t\t// in the Node table. Then drop the Route table.\n\t\t\t{\n\t\t\t\tID: \"202502131714\",\n\t\t\t\tMigrate: func(tx *gorm.DB) error {\n\t\t\t\t\tif !tx.Migrator().HasColumn(&types.Node{}, \"approved_routes\") {\n\t\t\t\t\t\terr := tx.Migrator().AddColumn(&types.Node{}, \"approved_routes\")\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"adding column types.Node: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tnodeRoutes := map[uint64][]netip.Prefix{}\n\n\t\t\t\t\tvar routes []types.Route //nolint:staticcheck // SA1019: Route kept for migrations\n\n\t\t\t\t\terr = tx.Find(&routes).Error\n\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}","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/db.go#L146-L182","documentation":"Migration 202502131714 failed reading all rows from the legacy routes table (tx.Find(&routes)) before converting enabled routes into per-node approved route lists. This is a SELECT failure: connection dropped, lock timeout, or rows that cannot scan into types.Route (e.g. malformed prefix data written by a fork).","triggerScenarios":"Query timeout on a large routes table; Postgres connection reset mid-migration; a prefix column value that netip.Prefix cannot parse when scanned.","commonSituations":"Upgrading databases with many thousands of routes; network blips between headscale and Postgres; databases previously touched by modified builds that wrote invalid prefix strings.","solutions":["Inspect the wrapped error - a scan/parse error names the offending row; a timeout suggests lock contention.","Increase the statement timeout / retry during a quiet window.","Back up, then manually inspect the routes table for malformed prefix values and fix or delete them.","Retry startup - the migration runs in a transaction, so no partial state persists."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if _, err := db.NewHeadscaleDatabase(cfg); err != nil {\n    if strings.Contains(err.Error(), \"fetching routes\") {\n        // SELECT failed: inspect wrapped error for scan errors (bad prefix data) vs timeouts\n    }\n}","preventionTips":["Keep route advertisements sane; prune unused subnet routes before major upgrades.","Ensure stable DB connectivity during startup migrations.","Never write non-prefix strings into the routes table with external tools."],"tags":["database","migration","routes","scan","postgres"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}