grpc/grpc-go · error

xds: unable to unmarshal lbconfig

Error message

xds: unable to unmarshal lbconfig: %s, error: %v

What it means

Returned by bb.ParseConfig (line 112) when json.Unmarshal fails on the raw JSON load balancer config passed to the CDS balancer's config parser. The %s shows the raw JSON input and %v shows the unmarshalling error. This config is the {"cluster": "...", "isDynamic": ...} structure defined by the lbConfig struct at lines 101-105.

Solutions

  1. Inspect the full error message which includes the raw JSON (%s) — the malformed portion is usually visible there.
  2. Validate that the JSON matches {"cluster": "<string>", "isDynamic": <bool>} exactly.
  3. Do not hand-construct CDS balancer config; rely on the xDS resolver to generate it.
  4. In tests, use json.Marshal on a properly constructed lbConfig struct rather than string concatenation.

Example fix

// before: hand-built JSON with a typo
raw := []byte(`{"cluster":"my-cluster","isDyanmic":true}`) // 'isDyanmic' misspelled

// after: construct via struct + json.Marshal
cfg := lbConfig{ClusterName: "my-cluster", IsDynamic: true}
raw, _ := json.Marshal(cfg)
Defensive patterns

Strategy: validation

Validate before calling

// Validate CDS LB config JSON before passing it to ParseConfig
func validateCDSConfig(raw json.RawMessage) error {
    var cfg struct {
        ClusterName string `json:"cluster"`
        IsDynamic   bool   `json:"isDynamic"`
    }
    if err := json.Unmarshal(raw, &cfg); err != nil {
        return fmt.Errorf("invalid CDS config JSON: %w", err)
    }
    if cfg.ClusterName == "" {
        return fmt.Errorf("CDS config missing 'cluster' field")
    }
    return nil
}

Try / catch

// Go: check the error from ParseConfig
cfg, err := cdsBuilder.ParseConfig(rawJSON)
if err != nil {
    if strings.Contains(err.Error(), "unable to unmarshal lbconfig") {
        log.Printf("CDS config JSON is invalid: %s", rawJSON)
        // fix the JSON source
    }
}

Prevention

When it happens

Trigger: The xDS resolver (or a test harness) calls the cds_experimental balancer's ParseConfig with malformed JSON, or JSON whose 'cluster'/'isDynamic' fields have wrong types. In production this is generated by the xdsResolver so should be well-formed; in tests or custom resolvers it can be passed bad data.

Common situations: A custom service config or test that constructs the CDS LB config JSON by hand with a syntax error or type mismatch (e.g. isDynamic as a string instead of bool). A version mismatch where the lbConfig struct fields changed but the caller sends old-format JSON.

Related errors


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

Appendix: source

Thrown at internal/xds/balancer/cdsbalancer/cdsbalancer.go:112

// Name returns the name of balancers built by this builder.
func (bb) Name() string {
	return cdsName
}

// lbConfig represents the loadBalancingConfig section of the service config
// for the cdsBalancer.
type lbConfig struct {
	serviceconfig.LoadBalancingConfig
	ClusterName string `json:"cluster"`
	IsDynamic   bool   `json:"isDynamic"`
}

// ParseConfig parses the JSON load balancer config provided into an
// internal form or returns an error if the config is invalid.
func (bb) ParseConfig(c json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
	var cfg lbConfig
	if err := json.Unmarshal(c, &cfg); err != nil {
		return nil, fmt.Errorf("xds: unable to unmarshal lbconfig: %s, error: %v", string(c), err)
	}
	return &cfg, nil
}

// cdsBalancer implements a CDS based LB policy. It instantiates a
// cluster_resolver balancer to further resolve the serviceName received from
// CDS, into localities and endpoints. Implements the balancer.Balancer
// interface which is exposed to gRPC and implements the balancer.ClientConn
// interface which is exposed to the cluster_resolver balancer.
type cdsBalancer struct {
	// The following fields are initialized at build time and are either
	// read-only after that or provide their own synchronization, and therefore
	// do not need to be guarded by a mutex.
	cc                balancer.ClientConn   // ClientConn interface passed to child LB.
	bOpts             balancer.BuildOptions // BuildOptions passed to child LB.
	childConfigParser balancer.ConfigParser // Config parser for cluster_resolver LB policy.
	logger            *grpclog.PrefixLogger // Prefix logger for all logging.

View on GitHub (pinned to 0c51461d27)