micro/go-micro · error

require at least one node

Error message

require at least one node

What it means

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.

Source

Thrown at registry/etcd/etcd.go:161

	node := strings.ReplaceAll(id, "/", "-")
	return path.Join(prefix, service, node)
}

func servicePath(s string) string {
	return path.Join(prefix, strings.ReplaceAll(s, "/", "-"))
}

func (e *etcdRegistry) Init(opts ...registry.Option) error {
	return configure(e, opts...)
}

func (e *etcdRegistry) Options() registry.Options {
	return e.options
}

func (e *etcdRegistry) registerNode(s *registry.Service, node *registry.Node, opts ...registry.RegisterOption) error {
	if len(s.Nodes) == 0 {
		return errors.New("require at least one node")
	}

	// check existing lease cache
	e.RLock()
	leaseID, ok := e.leases[s.Name+node.Id]
	e.RUnlock()

	log := e.options.Logger

	if !ok {
		// missing lease, check if the key exists
		ctx, cancel := context.WithTimeout(context.Background(), e.options.Timeout)
		defer cancel()

		// look for the existing key
		rsp, err := e.client.Get(ctx, nodePath(s.Name, node.Id), clientv3.WithSerializable())
		if err != nil {
			return err

View on GitHub (pinned to 24529f1404)

Solutions

  1. Ensure Register is always called with at least one populated *registry.Node.
  2. Guard your registration wrapper: only call Register when len(s.Nodes) > 0.
  3. Verify node construction (address, port, id) from config isn't silently failing.
  4. Check for concurrent code that truncates/reassigns the Nodes slice before Register.

Example fix

// before
if len(svc.Nodes) == 0 { /* bug: register anyway */ }
reg.Register(svc)
// after
if len(svc.Nodes) == 0 {
    return fmt.Errorf("service %q has no nodes; not registering", svc.Name)
}
reg.Register(svc)
Defensive patterns

Strategy: validation

Validate before calling

func registerSafe(s *registry.Service) error {
    if len(s.Nodes) == 0 {
        return fmt.Errorf("refusing to register service %q with no nodes", s.Name)
    }
    return nil
}
// call registerSafe(s) before reg.Register(s)

Type guard

func hasNodes(s *registry.Service) bool { return s != nil && len(s.Nodes) > 0 }

Try / catch

if err := reg.Register(svc); err != nil && strings.Contains(err.Error(), "require at least one node") {
    log.Error("etcd register rejected: empty node list", "service", svc.Name)
    return err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of micro/go-micro@24529f1404 (2026-09-01). Data as JSON: /api/errors/7f4cf4386ee95cb4. Report an issue: GitHub.