grpc/grpc-go · error

rls: invalid childPolicy: entry

Error message

rls: invalid childPolicy: entry %v does not contain exactly 1 policy/config pair: %q

What it means

Emitted from parseChildPolicyConfigs (config.go:287) when a childPolicy array entry (a map[string]json.RawMessage) does not have exactly one key. Each entry must be a single {policyName: config} pair so the balancer can deterministically pick the policy.

Solutions

  1. Express each candidate policy as its own array entry: [{"round_robin":{}},{"pick_first":{}}]. The balancer picks the first registered one.
  2. Remove empty {} entries from the childPolicy array.
  3. Validate the childPolicy shape programmatically before applying (each element must have len == 1).

Example fix

// before
"childPolicy": [ { "round_robin": {}, "pick_first": {} } ]

// after
"childPolicy": [ { "round_robin": {} }, { "pick_first": {} } ]
Defensive patterns

Strategy: validation

Validate before calling

func validateChildPolicyEntries(policies []map[string]json.RawMessage) error {
    for i, e := range policies {
        if len(e) != 1 {
            return fmt.Errorf("childPolicy[%d] must have exactly one policy/config pair, got %d", i, len(e))
        }
    }
    return nil
}

Type guard

func childPolicyEntriesAreSingleKey(policies []map[string]json.RawMessage) bool {
    for _, e := range policies {
        if len(e) != 1 {
            return false
        }
    }
    return true
}

Prevention

When it happens

Trigger: A childPolicy entry is an empty object {}, or an object with two or more policy keys like {"round_robin":{}, "pick_first":{}}.

Common situations: Hand-authored config bundling fallback policies in one map entry instead of separate array entries; JSON merge of policy overrides producing multi-key entries; misunderstanding the childPolicy grammar (array of single-key maps, not a single multi-key map).

Related errors


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

Appendix: source

Thrown at balancer/rls/config.go:287

		cacheSizeBytes = maxCacheSize
	}
	return &lbConfig{
		kbMap:                kbMap,
		lookupService:        lookupService,
		lookupServiceTimeout: lookupServiceTimeout,
		maxAge:               maxAge,
		staleAge:             staleAge,
		cacheSizeBytes:       cacheSizeBytes,
		defaultTarget:        rlsProto.GetDefaultTarget(),
	}, nil
}

// parseChildPolicyConfigs iterates through the list of child policies and picks
// the first registered policy and validates its config.
func parseChildPolicyConfigs(childPolicies []map[string]json.RawMessage, targetFieldName string) (string, map[string]json.RawMessage, error) {
	for i, config := range childPolicies {
		if len(config) != 1 {
			return "", nil, fmt.Errorf("rls: invalid childPolicy: entry %v does not contain exactly 1 policy/config pair: %q", i, config)
		}

		var name string
		var rawCfg json.RawMessage
		for name, rawCfg = range config {
		}
		builder := balancer.Get(name)
		if builder == nil {
			continue
		}
		parser, ok := builder.(balancer.ConfigParser)
		if !ok {
			return "", nil, fmt.Errorf("rls: childPolicy %q with config %q does not support config parsing", name, string(rawCfg))
		}

		// To validate child policy configs we do the following:
		// - unmarshal the raw JSON bytes of the child policy config into a map
		// - add an entry with key set to `target_field_name` and a dummy value

View on GitHub (pinned to 0c51461d27)