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

  1. 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.
  2. Reproduce by unmarshaling the routeLookupConfig blob in isolation with protojson and read the field-level error message in %v.
  3. Upgrade the client (or the producer) so both sides use the same RouteLookupConfig schema version.
  4. 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

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


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)