{"record":{"id":"9e12e44bd3c46f1b","repo":"hashicorp/nomad","slug":"missing-node-id-for-client-deregistration","errorCode":null,"errorMessage":"missing node ID for client deregistration","messagePattern":"missing node ID for client deregistration","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/node_endpoint.go","lineNumber":531,"sourceCode":"func (n *Node) Deregister(args *structs.NodeDeregisterRequest, reply *structs.NodeUpdateResponse) error {\n\tauthErr := n.srv.Authenticate(n.ctx, args)\n\tif done, err := n.srv.forward(\"Node.Deregister\", args, args, reply); done {\n\t\treturn err\n\t}\n\tn.srv.MeasureRPCRate(\"node\", structs.RateMetricWrite, args)\n\tif authErr != nil {\n\t\treturn structs.ErrPermissionDenied\n\t}\n\tdefer metrics.MeasureSince([]string{\"nomad\", \"client\", \"deregister\"}, time.Now())\n\n\tif aclObj, err := n.srv.ResolveACL(args); err != nil {\n\t\treturn structs.ErrPermissionDenied\n\t} else if !aclObj.AllowNodeWrite() {\n\t\treturn structs.ErrPermissionDenied\n\t}\n\n\tif args.NodeID == \"\" {\n\t\treturn fmt.Errorf(\"missing node ID for client deregistration\")\n\t}\n\n\t// deregister takes a batch\n\trepack := &structs.NodeBatchDeregisterRequest{\n\t\tNodeIDs:      []string{args.NodeID},\n\t\tWriteRequest: args.WriteRequest,\n\t}\n\n\treturn n.deregister(repack, reply, func() (any, uint64, error) {\n\t\treturn n.srv.raftApply(structs.NodeDeregisterRequestType, args)\n\t})\n}\n\n// BatchDeregister is used to remove client nodes from the cluster.\nfunc (n *Node) BatchDeregister(args *structs.NodeBatchDeregisterRequest, reply *structs.NodeUpdateResponse) error {\n\tauthErr := n.srv.Authenticate(n.ctx, args)\n\tif done, err := n.srv.forward(\"Node.BatchDeregister\", args, args, reply); done {\n\t\treturn err","sourceCodeStart":513,"sourceCodeEnd":549,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/node_endpoint.go#L513-L549","documentation":"Guard in Node.Deregister: the deregistration request carries an empty NodeID, so there is no client node to remove from state.","triggerScenarios":"RPC call to Node.Deregister with NodeID set to \"\"; callers building requests programmatically and leaving NodeID unset after a failed lookup.","commonSituations":"Scripts deregistering nodes from stale inventories where the node id column is empty; automation passing an unbound variable into the request struct.","solutions":["Populate args.NodeID with the target node's UUID before calling Deregister","Look up the node by name via the node list API to obtain its ID first","Guard the call site: skip/flag nodes whose ID is empty","Use BatchDeregister with a filtered non-empty ID list if deregistering many"],"exampleFix":"// before\nreq := &api.NodeDeregisterRequest{NodeID: nodeIDFromInventory}\n// after\nif nodeIDFromInventory == \"\" {\n    return fmt.Errorf(\"skip: empty node id\")\n}\nreq := &api.NodeDeregisterRequest{NodeID: nodeIDFromInventory}","handlingStrategy":"validation","validationCode":"if nodeID == \"\" {\n    return fmt.Errorf(\"refusing to deregister: empty node id\")\n}","typeGuard":"func hasNodeID(id string) bool { return id != \"\" }","tryCatchPattern":"err := client.Nodes().Deregister(nodeID, nil)\nif err != nil && strings.Contains(err.Error(), \"missing node ID\") {\n    // fix caller's data pipeline; no server action needed\n}","preventionTips":["Resolve node IDs from the nodes list API, not hand-maintained inventories","Validate inputs at the boundary of your automation","Skip records with empty IDs explicitly"],"tags":["nomad","validation","deregistration"],"backgroundTag":"missing-required-argument","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}