grpc/grpc-go · error

xds: failed to create a new channel for server config

Error message

xds: failed to create a new channel for server config %v: %v

What it means

newXDSChannel returned an error when constructing the xdsChannel for a server config. newXDSChannel (channel.go:79-89) validates that transport, serverConfig, clientConfig, and eventHandler are all non-nil. This wraps that internal validation error, meaning one of the required channel components was nil at creation time.

Solutions

  1. If using a custom TransportBuilder, verify Build never returns (nil, nil)
  2. Ensure the XDSClient is not used after Close() returns
  3. If this occurs with unmodified gRPC, report it as a bug with a goroutine dump
Defensive patterns

Strategy: validation

Validate before calling

// Wrap TransportBuilder to guard against nil-transport-with-nil-error returns.
type safeTransportBuilder struct{ inner clients.TransportBuilder }
func (s safeTransportBuilder) Build(si clients.ServerIdentifier) (clients.Transport, error) {
    t, err := s.inner.Build(si)
    if err == nil && t == nil {
        return nil, fmt.Errorf("TransportBuilder returned nil transport for %v", si)
    }
    return t, err
}
config.TransportBuilder = safeTransportBuilder{inner: config.TransportBuilder}

Prevention

When it happens

Trigger: Called right after a transport is successfully built at xdsclient.go:289, but newXDSChannel at line 298 rejects one of the xdsChannelOpts fields as nil (channel.go:81-88). Since transport was just built and serverConfig/clientConfig come from the already-validated XDSClient, this is an internal invariant violation.

Common situations: A concurrent Close() racing with channel creation causing a field to become nil; a custom TransportBuilder returning (nil, nil) that bypasses the Build error check; internal gRPC bug in non-standard usage. Extremely rare in standard gRPC xDS client operation.

Related errors


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

Appendix: source

Thrown at internal/xds/clients/xdsclient/xdsclient.go:308

	if err != nil {
		return nil, func() {}, fmt.Errorf("xds: failed to create transport for server config %v: %v", serverConfig, err)
	}
	state := &channelState{
		parent:                c,
		serverConfig:          serverConfig,
		interestedAuthorities: make(map[*authority]bool),
	}
	channel, err := newXDSChannel(xdsChannelOpts{
		transport:          tr,
		serverConfig:       serverConfig,
		clientConfig:       c.config,
		eventHandler:       state,
		backoff:            c.backoff,
		watchExpiryTimeout: c.watchExpiryTimeout,
		logPrefix:          clientPrefix(c),
	})
	if err != nil {
		return nil, func() {}, fmt.Errorf("xds: failed to create a new channel for server config %v: %v", serverConfig, err)
	}
	state.channel = channel
	c.xdsActiveChannels[*serverConfig] = state
	initLocked(state)
	return state.channel, c.releaseChannel(serverConfig, state, deInitLocked), nil
}

// releaseChannel is a function that is called when a reference to an xdsChannel
// needs to be released. It handles closing channels with no active references.
//
// The function takes the following parameters:
// - serverConfig: the server configuration for the xdsChannel
// - state: the state of the xdsChannel
// - deInitLocked: a function that performs any necessary cleanup for the xdsChannel
//
// The function returns another function that can be called to release the
// reference to the xdsChannel. This returned function is idempotent, meaning
// it can be called multiple times without any additional effect.

View on GitHub (pinned to 0c51461d27)