grpc/grpc-go · error

xds: unable to unmarshal LBconfig

Error message

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

What it means

ParseConfig for outlier_detection json.Unmarshal's the raw bytes into *LBConfig (balancer.go:111-113). If unmarshal fails (malformed JSON, wrong field types, invalid duration string, unknown enum), parsing is aborted and the original bytes plus error are returned.

Solutions

  1. Validate the raw JSON with a JSON parser before calling ParseConfig.
  2. Cross-check field types against the LBConfig struct (Interval is a duration string, percentages are uint32).
  3. For xDS deployments, investigate why xdsclient let the malformed resource through.

Example fix

// before
rawJSON := []byte(`{"interval": "not-a-duration"}`)
cfg, err := outlierdetection.ParseConfig(rawJSON) // errors
// after
rawJSON := []byte(`{"interval": "10s"}`)
cfg, err := outlierdetection.ParseConfig(rawJSON)
Defensive patterns

Strategy: validation

Validate before calling

// Validate raw JSON before calling ParseConfig.
func validateOutlierDetectionJSON(raw json.RawMessage) error {
    var probe map[string]json.RawMessage
    if err := json.Unmarshal(raw, &probe); err != nil {
        return fmt.Errorf("outlier_detection config is not valid JSON: %w", err)
    }
    return nil
}

Prevention

When it happens

Trigger: raw JSON is syntactically invalid, contains fields with wrong types (e.g. interval as a non-duration string), or has structurally incompatible nested objects (child policy config of wrong shape).

Common situations: Manually constructed outlier_detection JSON with typos; control plane sending malformed config (xdsclient should catch this upstream, so this is rare in production); test fixtures with bad JSON; field type drift after a gRPC upgrade.

Related errors


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

Appendix: source

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

	b.logger.Infof("Created")
	b.child = synchronizingBalancerWrapper{lb: gracefulswitch.NewBalancer(b, bOpts)}
	go b.run()
	return b
}

func (bb) ParseConfig(s json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
	lbCfg := &LBConfig{
		// Default top layer values as documented in A50.
		Interval:           iserviceconfig.Duration(10 * time.Second),
		BaseEjectionTime:   iserviceconfig.Duration(30 * time.Second),
		MaxEjectionTime:    iserviceconfig.Duration(300 * time.Second),
		MaxEjectionPercent: 10,
	}

	// This unmarshalling handles underlying layers sre and fpe which have their
	// own defaults for their fields if either sre or fpe are present.
	if err := json.Unmarshal(s, lbCfg); err != nil { // Validates child config if present as well.
		return nil, fmt.Errorf("xds: unable to unmarshal LBconfig: %s, error: %v", string(s), err)
	}

	// Note: in the xds flow, these validations will never fail. The xdsclient
	// performs the same validations as here on the xds Outlier Detection
	// resource before parsing resource into JSON which this function gets
	// called with. A50 defines two separate places for these validations to
	// take place, the xdsclient and this ParseConfig method. "When parsing a
	// config from JSON, if any of these requirements is violated, that should
	// be treated as a parsing error." - A50
	switch {
	// "The google.protobuf.Duration fields interval, base_ejection_time, and
	// max_ejection_time must obey the restrictions in the
	// google.protobuf.Duration documentation and they must have non-negative
	// values." - A50
	// Approximately 290 years is the maximum time that time.Duration (int64)
	// can represent. The restrictions on the protobuf.Duration field are to be
	// within +-10000 years. Thus, just check for negative values.
	case lbCfg.Interval < 0:

View on GitHub (pinned to 0c51461d27)