grpc/grpc-go · error
rls: bad RouteLookupConfig proto %+v
Error message
rls: bad RouteLookupConfig proto %+v: %v
What it means
Emitted from (rlsBB).ParseConfig (config.go:158) when protojson.Unmarshal of the routeLookupConfig JSON blob into rlspb.RouteLookupConfig fails. UnmarshalOptions has DiscardUnknown:true, so unknown fields are ignored; the error fires only for genuine proto incompatibilities such as a field whose JSON value type does not match the proto field type, or a malformed well-known type like a duration/struct value.
Solutions
- Compare the proto definition used by the config producer against google.golang.org/grpc/internal/proto/grpc_lookup_v1/rls_config.pb.go on the client.
- Reproduce by unmarshaling the routeLookupConfig blob in isolation with protojson and read the field-level error message in %v.
- Upgrade the client (or the producer) so both sides use the same RouteLookupConfig schema version.
- Strip any non-proto JSON fields from routeLookupConfig and ensure well-known types (Duration, Struct) use their canonical JSON form.
Example fix
// before: grpc_keybuilders given as an object instead of an array
"routeLookupConfig": { "grpcKeybuilders": { "names": [ { "service": "svc" } ] } }
// after: grpc_keybuilders is an array per the proto
"routeLookupConfig": {
"grpcKeybuilders": [ { "names": [ { "service": "pkg.svc" } ], "headers": [ { "key": "k" } ] } ]
} Defensive patterns
Strategy: validation
Validate before calling
// Confirm the routeLookupConfig blob parses as the proto on the client build.
func validateRouteLookupProto(raw json.RawMessage) error {
var p rlspb.RouteLookupConfig
opts := protojson.UnmarshalOptions{DiscardUnknown: true}
if err := opts.Unmarshal(raw, &p); err != nil {
return fmt.Errorf("routeLookupConfig is not a valid RouteLookupConfig proto: %w", err)
}
return nil
} Type guard
func isRouteLookupProto(raw json.RawMessage) bool {
var p rlspb.RouteLookupConfig
return protojson.UnmarshalOptions{DiscardUnknown: true}.Unmarshal(raw, &p) == nil
} Prevention
- Keep the config producer and client on the same grpc_lookup_v1 proto version.
- Use protojson.Marshal on the producer side to generate the routeLookupConfig blob rather than hand-serializing.
- Add a contract test that round-trips the produced config through the client's ParseConfig.
When it happens
Trigger: The routeLookupConfig object contains a field with the wrong JSON type, e.g. grpc_keybuilders as a string instead of an array, or a numeric field given an object; or an embedded google.protobuf.Duration/Struct field carrying an invalid JSON encoding.
Common situations: Proto schema version skew between the config producer (xDS server, control plane) and the client's grpc_lookup_v1 proto; a field renamed/retyped across versions (e.g. an older field shape still emitted); a control-plane bug serializing a Go struct directly instead of the proto.
Related errors
- rls: bad control channel service config
- 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: failed to parse lookup_service_timeout in route lookup…
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/16315a81aa37a37d.
Report an issue: GitHub.
Appendix: source
Thrown at balancer/rls/config.go:158
// - childPolicy:
// - must find a valid child policy with a valid config
//
// - childPolicyConfigTargetFieldName:
// - must be set and non-empty
func (rlsBB) ParseConfig(c json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
if logger.V(2) {
logger.Infof("Received JSON service config: %v", pretty.ToJSON(c))
}
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)View on GitHub (pinned to 0c51461d27)