grpc/grpc-go · error
rls: bad control channel service config
Error message
rls: bad control channel service config %q: %v
What it means
Emitted from (rlsBB).ParseConfig (config.go:168) when routeLookupChannelServiceConfig is non-empty but internal.ParseServiceConfig reports an error. This service config is applied to the control channel (the gRPC channel the RLS balancer uses to talk to the Route Lookup Server), so it must itself be a valid gRPC service config.
Solutions
- Validate the routeLookupChannelServiceConfig value independently with grpc.NewClient and a service config parse, or with the clientconf test harness.
- If the control channel needs no special config, omit routeLookupChannelServiceConfig entirely (the empty case is allowed and skips this check).
- Ensure any LB policy referenced inside the control-channel config is blank-imported by the binary (e.g. _ 'google.golang.org/grpc/balancer/roundrobin').
- Simplify the control-channel config to a minimal valid service config and re-add fields one at a time to isolate the failing entry.
Example fix
// before: unknown policy on control channel
"routeLookupChannelServiceConfig": { "loadBalancingConfig": [ { "not_a_real_policy": {} } ] }
// after: omit it, or use a registered policy
// (omit the field entirely)
// or:
"routeLookupChannelServiceConfig": { "loadBalancingConfig": [ { "round_robin": {} } ] } Defensive patterns
Strategy: validation
Validate before calling
// Validate the optional control-channel service config independently.
func validateControlChannelSC(raw json.RawMessage) error {
if len(raw) == 0 {
return nil // allowed: empty means no control-channel override
}
res := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(string(raw))
return res.Err
} Type guard
func isControlChannelSCValid(raw json.RawMessage) bool {
if len(raw) == 0 {
return true
}
res := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(string(raw))
return res.Err == nil
} Prevention
- Omit routeLookupChannelServiceConfig when the control channel needs no special policy.
- Blank-import any LB policy referenced inside the control-channel config.
- Validate nested service configs with the same parser the channel uses before deploying.
When it happens
Trigger: Providing a routeLookupChannelServiceConfig blob that is invalid JSON or references an unknown load balancing policy, an unknown retry policy, or otherwise fails gRPC service-config parsing.
Common situations: Setting a control-channel LB policy (e.g. 'xdsmetadata_experimental', 'grpclb') that the client binary does not have linked in; malformed JSON inside the nested config; copying a config intended for the data channel that includes fields the control channel rejects.
Related errors
- rls: bad RouteLookupConfig proto %+v
- rls: cache_size_bytes must be set to a non-zero value: %+v
- rls: childPolicyConfigTargetFieldName field is not set in…
- rls: empty lookup_service in route lookup config %+v
- rls: invalid childPolicy: entry
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/b72f7ffa3506e067.
Report an issue: GitHub.
Appendix: source
Thrown at balancer/rls/config.go:168
cfgJSON := &lbConfigJSON{}
if err := json.Unmarshal(c, cfgJSON); err != nil {
return nil, fmt.Errorf("rls: json unmarshal failed for service config %+v: %v", string(c), err)
}
m := protojson.UnmarshalOptions{DiscardUnknown: true}
rlsProto := &rlspb.RouteLookupConfig{}
if err := m.Unmarshal(cfgJSON.RouteLookupConfig, rlsProto); err != nil {
return nil, fmt.Errorf("rls: bad RouteLookupConfig proto %+v: %v", string(cfgJSON.RouteLookupConfig), err)
}
lbCfg, err := parseRLSProto(rlsProto)
if err != nil {
return nil, err
}
if sc := string(cfgJSON.RouteLookupChannelServiceConfig); sc != "" {
parsed := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(sc)
if parsed.Err != nil {
return nil, fmt.Errorf("rls: bad control channel service config %q: %v", sc, parsed.Err)
}
lbCfg.controlChannelServiceConfig = sc
}
if cfgJSON.ChildPolicyConfigTargetFieldName == "" {
return nil, fmt.Errorf("rls: childPolicyConfigTargetFieldName field is not set in service config %+v", string(c))
}
name, config, err := parseChildPolicyConfigs(cfgJSON.ChildPolicy, cfgJSON.ChildPolicyConfigTargetFieldName)
if err != nil {
return nil, err
}
lbCfg.childPolicyName = name
lbCfg.childPolicyConfig = config
lbCfg.childPolicyTargetField = cfgJSON.ChildPolicyConfigTargetFieldName
return lbCfg, nil
}
func parseRLSProto(rlsProto *rlspb.RouteLookupConfig) (*lbConfig, error) {View on GitHub (pinned to 0c51461d27)