{"record":{"id":"7f4cf4386ee95cb4","repo":"micro/go-micro","slug":"require-at-least-one-node-7f4cf4","errorCode":null,"errorMessage":"require at least one node","messagePattern":"require at least one node","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"registry/etcd/etcd.go","lineNumber":161,"sourceCode":"\tnode := strings.ReplaceAll(id, \"/\", \"-\")\n\treturn path.Join(prefix, service, node)\n}\n\nfunc servicePath(s string) string {\n\treturn path.Join(prefix, strings.ReplaceAll(s, \"/\", \"-\"))\n}\n\nfunc (e *etcdRegistry) Init(opts ...registry.Option) error {\n\treturn configure(e, opts...)\n}\n\nfunc (e *etcdRegistry) Options() registry.Options {\n\treturn e.options\n}\n\nfunc (e *etcdRegistry) registerNode(s *registry.Service, node *registry.Node, opts ...registry.RegisterOption) error {\n\tif len(s.Nodes) == 0 {\n\t\treturn errors.New(\"require at least one node\")\n\t}\n\n\t// check existing lease cache\n\te.RLock()\n\tleaseID, ok := e.leases[s.Name+node.Id]\n\te.RUnlock()\n\n\tlog := e.options.Logger\n\n\tif !ok {\n\t\t// missing lease, check if the key exists\n\t\tctx, cancel := context.WithTimeout(context.Background(), e.options.Timeout)\n\t\tdefer cancel()\n\n\t\t// look for the existing key\n\t\trsp, err := e.client.Get(ctx, nodePath(s.Name, node.Id), clientv3.WithSerializable())\n\t\tif err != nil {\n\t\t\treturn err","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/micro/go-micro/blob/24529f140421a11a33b6999ab7944f2021cfd69c/registry/etcd/etcd.go#L143-L179","documentation":"etcdRegistry.registerNode performs the same guard: it refuses to register a node when the parent service's Nodes slice is empty, because lease keys are derived from service name + node ID. It is invoked from Register for each node and fails fast on an empty set.","triggerScenarios":"Register called with an empty Nodes slice reaches registerNode and returns this error; also possible when Register builds the node list dynamically and the loop that adds nodes never executes.","commonSituations":"Programmatic registration code with an empty node list; configuration missing address/port so node construction is skipped; race where node list is reset before Register is invoked.","solutions":["Ensure Register is always called with at least one populated *registry.Node.","Guard your registration wrapper: only call Register when len(s.Nodes) > 0.","Verify node construction (address, port, id) from config isn't silently failing.","Check for concurrent code that truncates/reassigns the Nodes slice before Register."],"exampleFix":"// before\nif len(svc.Nodes) == 0 { /* bug: register anyway */ }\nreg.Register(svc)\n// after\nif len(svc.Nodes) == 0 {\n    return fmt.Errorf(\"service %q has no nodes; not registering\", svc.Name)\n}\nreg.Register(svc)","handlingStrategy":"validation","validationCode":"func registerSafe(s *registry.Service) error {\n    if len(s.Nodes) == 0 {\n        return fmt.Errorf(\"refusing to register service %q with no nodes\", s.Name)\n    }\n    return nil\n}\n// call registerSafe(s) before reg.Register(s)","typeGuard":"func hasNodes(s *registry.Service) bool { return s != nil && len(s.Nodes) > 0 }","tryCatchPattern":"if err := reg.Register(svc); err != nil && strings.Contains(err.Error(), \"require at least one node\") {\n    log.Error(\"etcd register rejected: empty node list\", \"service\", svc.Name)\n    return err\n}","preventionTips":["Ensure every Register call includes at least one populated Node","Check that node-building code isn't silently skipping nodes on missing config","Avoid concurrent mutation of the Nodes slice before Register","Test the register path with a service fixture containing exactly one node"],"tags":["etcd","registry","register","validation"],"backgroundTag":"missing-service-nodes","analyzedSha":"24529f140421a11a33b6999ab7944f2021cfd69c","analyzedAt":"2026-09-01T02:52:24.923Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}