grpc/grpc-go · error

xdsclient: eventHandler is nil

Error message

xdsclient: eventHandler is nil

What it means

The final mandatory field checked by `newXDSChannel` (channel.go:79) is `eventHandler` — an implementation of the xdsChannelEventHandler interface that receives callbacks for ADS stream events (resource updates, errors, resource-does-not-exist). Because the channel has no other way to deliver these events, a nil handler is rejected at channel.go:87-88.

Source

Thrown at internal/xds/clients/xdsclient/channel.go:88

	eventHandler       xdsChannelEventHandler  // Callbacks for ADS stream events.
	backoff            func(int) time.Duration // Backoff function to use for stream retries. Defaults to exponential backoff, if unset.
	watchExpiryTimeout time.Duration           // Timeout for ADS resource watch expiry.
	logPrefix          string                  // Prefix to use for logging.
}

// newXDSChannel creates a new xdsChannel instance with the provided options.
// It performs basic validation on the provided options and initializes the
// xdsChannel with the necessary components.
func newXDSChannel(opts xdsChannelOpts) (*xdsChannel, error) {
	switch {
	case opts.transport == nil:
		return nil, errors.New("xdsclient: transport is nil")
	case opts.serverConfig == nil:
		return nil, errors.New("xdsclient: serverConfig is nil")
	case opts.clientConfig == nil:
		return nil, errors.New("xdsclient: clientConfig is nil")
	case opts.eventHandler == nil:
		return nil, errors.New("xdsclient: eventHandler is nil")
	}

	xc := &xdsChannel{
		transport:    opts.transport,
		serverConfig: opts.serverConfig,
		clientConfig: opts.clientConfig,
		eventHandler: opts.eventHandler,
		closed:       syncutil.NewEvent(),
	}

	l := grpclog.Component("xds")
	logPrefix := opts.logPrefix + fmt.Sprintf("[xds-channel %p] ", xc)
	xc.logger = igrpclog.NewPrefixLogger(l, logPrefix)

	if opts.backoff == nil {
		opts.backoff = backoff.DefaultExponential.Backoff
	}
	np := internal.NodeProto(opts.clientConfig.Node)

View on GitHub (pinned to 0c51461d27)

Solutions

  1. Pass a non-nil xdsChannelEventHandler (typically the owning authority) in xdsChannelOpts.eventHandler.
  2. For tests, supply a no-op handler struct that satisfies the interface rather than leaving it nil.
  3. Audit any custom code that constructs xdsChannelOpts to confirm eventHandler is set.

Example fix

// before
opts := xdsChannelOpts{transport: t, serverConfig: sc, clientConfig: cc}
xc, err := newXDSChannel(opts) // err: eventHandler is nil

// after
type noopHandler struct{}
func (noopHandler) OnResourceUpdate(rt ResourceType, n string, d xdsresource.ResourceData) {}
func (noopHandler) OnResourceError(rt ResourceType, n string, err error) {}
func (noopHandler) OnStreamError(err error) {}
opts := xdsChannelOpts{transport: t, serverConfig: sc, clientConfig: cc, eventHandler: noopHandler{}}
Defensive patterns

Strategy: type-guard

Type guard

// Compile-time assertion that your handler satisfies the interface.
var _ xdsChannelEventHandler = (*myHandler)(nil)

type myHandler struct{}
func (*myHandler) OnResourceUpdate(rt ResourceType, n string, d xdsresource.ResourceData) {}
func (*myHandler) OnResourceError(rt ResourceType, n string, err error)                 {}
func (*myHandler) OnStreamError(err error)                                            {}
func (*myHandler) adsResourceDoesNotExist(rt ResourceType, n string)                   {}

Prevention

When it happens

Trigger: Triggered when `newXChannel` is called with `opts.eventHandler == nil`. The XDSClient normally supplies an authority (which implements the interface) as the handler; a nil means the wiring was skipped.

Common situations: Calling newXDSChannel from a test stub without supplying an event handler; refactoring the authority wiring so the handler is no longer passed; a custom resource-type integration that bypasses the authority abstraction.

Related errors


AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11). Data as JSON: /api/errors/098c3afd2daf4590. Report an issue: GitHub.