grpc/grpc-go · error

unexpected balancer config with type: %T

Error message

unexpected balancer config with type: %T

What it means

priority's UpdateClientConnState asserts s.BalancerConfig is *priority.LBConfig (balancer.go:121-123). Any other type is rejected because priority needs the Children map and ordered Priorities list to manage its sub-balancers.

Source

Thrown at internal/xds/balancer/priority/balancer.go:123

	priorities []string
	// children is a map from child name to sub-balancers.
	children map[string]*childBalancer

	// Set during UpdateClientConnState when calling into sub-balancers.
	// Prevents child updates from recomputing the active priority or sending
	// an update of the aggregated picker to the parent.  Cleared after all
	// sub-balancers have finished UpdateClientConnState, after which
	// syncPriority is called manually.
	inhibitPickerUpdates bool
}

func (b *priorityBalancer) UpdateClientConnState(s balancer.ClientConnState) error {
	if b.logger.V(2) {
		b.logger.Infof("Received an update with 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)
	}
	endpointsSplit := hierarchy.Group(s.ResolverState.Endpoints)

	b.mu.Lock()
	// Create and remove children, since we know all children from the config
	// are used by some priority.
	for name, newSubConfig := range newConfig.Children {
		bb := balancer.Get(newSubConfig.Config.Name)
		if bb == nil {
			b.logger.Errorf("balancer name %v from config is not registered", newSubConfig.Config.Name)
			continue
		}

		currentChild, ok := b.children[name]
		if !ok {
			// This is a new child, add it to the children list. But note that
			// the balancer isn't built, because this child can be a low
			// priority. If necessary, it will be built when syncing priorities.

View on GitHub (pinned to 0c51461d27)

Solutions

  1. Always route config through priority.ParseConfig(rawJSON) to produce *LBConfig.
  2. Ensure the resolver's service config produces the priority_experimental policy with the expected Children/Priorities JSON shape.
  3. Verify the parent policy forwards the exact *LBConfig returned by ParseConfig.

Example fix

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

Strategy: type-guard

Validate before calling

func validatePriorityConfig(cfg serviceconfig.LoadBalancingConfig) error {
    if _, ok := cfg.(*priority.LBConfig); !ok {
        return fmt.Errorf("expected *priority.LBConfig, got %T", cfg)
    }
    return nil
}

Type guard

func isPriorityConfig(cfg serviceconfig.LoadBalancingConfig) bool {
    _, ok := cfg.(*priority.LBConfig)
    return ok
}

Prevention

When it happens

Trigger: UpdateClientConnState called on the priority balancer with a BalancerConfig that is not *priority.LBConfig; usually means the xDS resolver produced the wrong config or a custom parent bypassed ParseConfig.

Common situations: Custom resolver that does not use priority.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/253fa02406ef5817. Report an issue: GitHub.