grpc/grpc-go · error
ringhash: expected xDS config selector to set the request…
Error message
ringhash: expected xDS config selector to set the request hash
What it means
Returned by the ringhash picker's Pick() (balancer/ringhash/picker.go:61) when requestHashHeader is empty (xDS-hash mode) but iringhash.XDSRequestHash(ctx) returns ok == false. In xDS mode, the request hash is expected to be injected into the RPC context by the xDS config selector so that the same client consistently picks the same endpoint. Its absence means the xDS pipeline did not set the hash.
Solutions
- Use ring_hash only within a full xDS-managed channel (xds.NewClientConn) so the config selector sets the hash.
- Alternatively, set requestHashHeader in the config so the picker hashes an outgoing metadata value instead of requiring xDS injection.
- Avoid interceptors that replace the RPC context without preserving the xDS hash.
Example fix
// before: plain grpc client with ring_hash and no requestHashHeader → xDS hash missing
conn, _ := grpc.NewClient("dns:///svc:50051", grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"ring_hash_experimental":{}}]}`))
// after: use an xDS-managed channel that injects the request hash, OR set requestHashHeader
conn, _ := xds.NewClientConn("xds:///svc", xds.WithServiceConfig(...))
// or, for non-xDS, provide a header-based hash:
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"ring_hash_experimental":{"requestHashHeader":"user-id"}}]}`) Defensive patterns
Strategy: validation
Validate before calling
// Prefer a header-based hash in non-xDS setups to avoid the missing
// xDS-hash problem entirely.
func safeRingHashConfig() string {
return `{"loadBalancingConfig":[{"ring_hash_experimental":{"requestHashHeader":"user-id"}}]}`
} Try / catch
// If you cannot guarantee the xDS hash, catch the Unavailable and
// fall back to a header-based hash or a different policy.
resp, err := c.Call(ctx, in)
if err != nil && strings.Contains(err.Error(), "expected xDS config selector") {
// reconfigure channel with requestHashHeader or switch policy
}
return resp, err Prevention
- Use ring_hash only through an xDS-managed channel (xds.NewClientConn).
- Set requestHashHeader when running ring_hash without xDS so the picker hashes outgoing metadata.
- Do not write interceptors that discard the incoming context (and its xDS hash attribute).
When it happens
Trigger: The picker was built with requestHashHeader == "" (line 58), and at Pick time iringhash.XDSRequestHash(info.Ctx) returns (0, false) — no hash in the context. This happens when ring_hash is used through xDS but the config selector that normally sets the hash is absent or bypassed.
Common situations: Using ring_hash outside of a proper xDS configuration; the xDS RDS/RouteConfiguration lacks a hash policy; a custom interceptor that replaces the context drops the hash attribute; calling a ring_hash-backed channel directly without the xDS selector.
Related errors
- child policy not registered
- failed to JSON marshal load balancing policy for child
- failed to parse load balancing policy for child
- failed to push new configuration
- invalid requestHashHeader
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/78c3f3d29bb836c9.
Report an issue: GitHub.
Appendix: source
Thrown at balancer/ringhash/picker.go:61
// requestHashHeader is the header key to look for the request hash. If it's
// empty, the request hash is expected to be set in the context via xDS.
// See gRFC A76.
requestHashHeader string
// hasEndpointInConnectingState is true if any of the endpoints is in
// CONNECTING.
hasEndpointInConnectingState bool
randUint64 func() uint64
}
func (p *picker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
usingRandomHash := false
var requestHash uint64
if p.requestHashHeader == "" {
var ok bool
if requestHash, ok = iringhash.XDSRequestHash(info.Ctx); !ok {
return balancer.PickResult{}, fmt.Errorf("ringhash: expected xDS config selector to set the request hash")
}
} else {
md, ok := metadata.FromOutgoingContext(info.Ctx)
if !ok || len(md.Get(p.requestHashHeader)) == 0 {
requestHash = p.randUint64()
usingRandomHash = true
} else {
values := strings.Join(md.Get(p.requestHashHeader), ",")
requestHash = xxhash.Sum64String(values)
}
}
e := p.ring.pick(requestHash)
ringSize := len(p.ring.items)
if !usingRandomHash {
// Per gRFC A61, because of sticky-TF with PickFirst's auto reconnect on TF,
// we ignore all TF subchannels and find the first ring entry in READY,
// CONNECTING or IDLE. If that entry is in IDLE, we need to initiate aView on GitHub (pinned to 0c51461d27)