{"record":{"id":"296115ed24a14ef6","repo":"juanfont/headscale","slug":"renaming-node-w-296115","errorCode":null,"errorMessage":"renaming node: %w","messagePattern":"renaming node: %w","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"error","filePath":"hscontrol/db/node.go","lineNumber":190,"sourceCode":"\treturn hsdb.Write(func(tx *gorm.DB) error {\n\t\treturn SetLastSeen(tx, nodeID, lastSeen)\n\t})\n}\n\n// SetLastSeen sets a node's last seen field indicating that we\n// have recently communicating with this node.\nfunc SetLastSeen(tx *gorm.DB, nodeID types.NodeID, lastSeen time.Time) error {\n\treturn tx.Model(&types.Node{}).Where(\"id = ?\", nodeID).Update(\"last_seen\", lastSeen).Error\n}\n\n// RenameNode takes a [types.Node] struct and a new [types.Node.GivenName] for the nodes\n// and renames it. Validation should be done in the state layer before calling this function.\nfunc RenameNode(tx *gorm.DB,\n\tnodeID types.NodeID, newName string,\n) error {\n\terr := dnsname.ValidLabel(newName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"renaming node: %w\", err)\n\t}\n\n\t// Check if the new name is unique\n\tvar count int64\n\n\tif err := tx.Model(&types.Node{}).Where(\"given_name = ? AND id != ?\", newName, nodeID).Count(&count).Error; err != nil { //nolint:noinlineerr\n\t\treturn fmt.Errorf(\"checking name uniqueness: %w\", err)\n\t}\n\n\tif count > 0 {\n\t\treturn ErrNodeNameNotUnique\n\t}\n\n\tif err := tx.Model(&types.Node{}).Where(\"id = ?\", nodeID).Update(\"given_name\", newName).Error; err != nil { //nolint:noinlineerr\n\t\treturn fmt.Errorf(\"renaming node in database: %w\", err)\n\t}\n\n\treturn nil","sourceCodeStart":172,"sourceCodeEnd":208,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/node.go#L172-L208","documentation":"RenameNode validates the new GivenName with dnsname.ValidLabel before touching the database. This error means the proposed name is not a valid DNS label: empty, longer than 63 bytes, or containing characters outside the RFC 1035 subset. The comment states validation is expected to live in the state layer, so reaching this DB-layer check means the caller skipped it.","triggerScenarios":"Passing a raw user-supplied hostname with spaces, dots, or UTF-8 characters; a name of 64+ bytes; an empty string after trimming.","commonSituations":"gRPC/API rename call with unvalidated input; scripts or automation feeding hostnames straight from cloud instance metadata.","solutions":["Sanitize with dnsname.SanitizeHostname before calling RenameNode","Reject or truncate names to 63 bytes at the API boundary","Return a 400-style validation error to the client instead of surfacing the DB error"],"exampleFix":"// before\nerr := db.RenameNode(tx, nodeID, newName) // newName = \"my server #1\"\n\n// after\nname := dnsname.SanitizeHostname(newName)\nif name == \"\" {\n\treturn fmt.Errorf(\"invalid node name: %q\", newName)\n}\nerr := db.RenameNode(tx, nodeID, name)","handlingStrategy":"validation","validationCode":"name := dnsname.SanitizeHostname(newName)\nif name == \"\" || len(name) > 63 {\n\treturn fmt.Errorf(\"rejecting node name %q\", newName)\n}\nerr := db.RenameNode(tx, nodeID, name)","typeGuard":"func isValidNodeLabel(s string) bool {\n\treturn dnsname.ValidLabel(s) == nil\n}","tryCatchPattern":null,"preventionTips":["Sanitize at the API boundary, not the DB layer","Trim and length-check user input before rename","Map this error to a 400 in gRPC handlers"],"tags":["go","database","dns","validation","naming"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}