grpc/grpc-go · error

unexpected balancer config with type: %T

Error message

unexpected balancer config with type: %T

What it means

cluster_manager's UpdateClientConnState asserts s.BalancerConfig is *clustermanager.lbConfig (clustermanager.go:171-174). Any other type is rejected because cluster_manager needs the Children map and per-child policy configs to drive its sub-balancers via balancergroup.

Source

Thrown at internal/xds/balancer/clustermanager/clustermanager.go:173

	// If multiple sub-balancers run into errors, we will return only the last
	// one, which is still good enough, since the grpc channel will anyways
	// return this error as balancer.ErrBadResolver to the name resolver,
	// resulting in re-resolution attempts.
	return retErr

	// Adding or removing a sub-balancer will result in the
	// needUpdateStateOnResume bit to true which results in a picker update once
	// resumeStateUpdates() is called.
}

func (b *bal) UpdateClientConnState(s balancer.ClientConnState) error {
	if b.logger.V(2) {
		b.logger.Infof("Received update from resolver, balancer config: %+v", pretty.ToJSON(s.BalancerConfig))
	}

	newConfig, ok := s.BalancerConfig.(*lbConfig)
	if !ok {
		return fmt.Errorf("unexpected balancer config with type: %T", s.BalancerConfig)
	}

	b.stateAggregator.pauseStateUpdates()
	defer b.stateAggregator.resumeStateUpdates()
	return b.updateChildren(s, newConfig)
}

func (b *bal) ResolverError(err error) {
	b.bg.ResolverError(err)
}

func (b *bal) UpdateSubConnState(sc balancer.SubConn, state balancer.SubConnState) {
	b.logger.Errorf("UpdateSubConnState(%v, %+v) called unexpectedly", sc, state)
}

func (b *bal) Close() {
	b.stateAggregator.close()
	b.bg.Close()

View on GitHub (pinned to 0c51461d27)

Solutions

  1. Always route config through clustermanager.ParseConfig(rawJSON) to produce *lbConfig.
  2. Ensure the resolver's service config produces the xds_cluster_manager_experimental policy with the expected JSON children/priorities shape.
  3. Check that the parent policy sends the exact *lbConfig returned by ParseConfig, not a wrapped or translated type.

Example fix

// before
b.UpdateClientConnState(balancer.ClientConnState{
    BalancerConfig: &someOtherConfig{},
})
// after
import "google.golang.org/grpc/internal/xds/balancer/clustermanager"
cfg, err := clustermanager.ParseConfig(rawJSON)
if err != nil { return err }
b.UpdateClientConnState(balancer.ClientConnState{BalancerConfig: cfg})
Defensive patterns

Strategy: type-guard

Validate before calling

func validateClusterManagerConfig(cfg serviceconfig.LoadBalancingConfig) error {
    // *clustermanager.lbConfig is unexported; the public route is ParseConfig.
    // Round-trip check: marshal then ParseConfig again.
    raw, err := json.Marshal(cfg)
    if err != nil { return err }
    _, err = clustermanager.ParseConfig(raw)
    return err
}

Type guard

// lbConfig is unexported, so callers cannot name it. Use ParseConfig to obtain the only valid value.
// In tests you can rely on the fact that bb.ParseConfig returns the only accepted type.

Prevention

When it happens

Trigger: UpdateClientConnState called on cluster_manager with a non-*lbConfig BalancerConfig; the xDS resolver produced the wrong shape or a custom parent bypassed clustermanager.ParseConfig.

Common situations: Custom resolver that doesn't use clustermanager.ParseConfig; gRPC version drift; tests that pass a raw struct.

Related errors


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