{"record":{"id":"97f5289708b2ad4a","repo":"hashicorp/nomad","slug":"missing-node-ids-for-client-deregistration","errorCode":null,"errorMessage":"missing node IDs for client deregistration","messagePattern":"missing node IDs for client deregistration","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/node_endpoint.go","lineNumber":564,"sourceCode":"func (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\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\", \"batch_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 len(args.NodeIDs) == 0 {\n\t\treturn fmt.Errorf(\"missing node IDs for client deregistration\")\n\t}\n\n\treturn n.deregister(args, reply, func() (any, uint64, error) {\n\t\treturn n.srv.raftApply(structs.NodeBatchDeregisterRequestType, args)\n\t})\n}\n\n// deregister takes a raftMessage closure, to support both Deregister and\n// BatchDeregister. The caller should have already authorized the request.\nfunc (n *Node) deregister(args *structs.NodeBatchDeregisterRequest,\n\treply *structs.NodeUpdateResponse,\n\traftApplyFn func() (any, uint64, error),\n) error {\n\t// Look for the node\n\tsnap, err := n.srv.fsm.State().Snapshot()\n\tif err != nil {\n\t\treturn err\n\t}","sourceCodeStart":546,"sourceCodeEnd":582,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/node_endpoint.go#L546-L582","documentation":"Guard in Node.BatchDeregister: the batch request contains no node IDs; at least one node ID is required to deregister clients.","triggerScenarios":"Calling BatchDeregister with a zero-length NodeIDs slice — e.g., a batch job whose input filter matched nothing and passed the empty result straight through.","commonSituations":"Bulk-cleanup automation where the query for dead nodes returned no rows; off-by-one filters; empty CSV input.","solutions":["Check len(NodeIDs) > 0 before issuing the RPC and no-op otherwise","Fix upstream filtering that produced an empty list unexpectedly","For a single node, use Node.Deregister instead","Log and alert when a scheduled cleanup finds zero nodes rather than sending the request"],"exampleFix":"// before\nclient.Nodes().BatchDeregister(&api.NodeBatchDeregisterRequest{NodeIDs: ids})\n// after\nif len(ids) == 0 {\n    return nil // nothing to deregister\n}\nclient.Nodes().BatchDeregister(&api.NodeBatchDeregisterRequest{NodeIDs: ids})","handlingStrategy":"validation","validationCode":"if len(nodeIDs) == 0 {\n    return nil // nothing to do\n}","typeGuard":"func hasIDs(ids []string) bool { return len(ids) > 0 }","tryCatchPattern":"err := client.Nodes().BatchDeregister(req, nil)\nif err != nil && strings.Contains(err.Error(), \"missing node IDs\") {\n    // empty batch: treat as no-op\n}","preventionTips":["Guard batch jobs for empty result sets before issuing RPCs","Log when a cleanup pass finds zero nodes","Prefer no-op over empty batch requests"],"tags":["nomad","validation","batch","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"}