etcd-io/etcd · error
`WithPrefix` and `WithFromKey` cannot be set at the same tim
Error message
`WithPrefix` and `WithFromKey` cannot be set at the same time, choose one
What it means
clientv3.OpGet panics when the option list contains both WithPrefix and WithFromKey. The two options compute conflicting range semantics: WithPrefix sets an end key derived from the key prefix (keys up to the next lexicographic prefix), while WithFromKey treats the key as the inclusive lower bound with end \0 (all keys >= key). etcd cannot honor both, so OpGet fails fast instead of silently picking one.
Source
Thrown at client/v3/op.go:260
for _, tOp := range op.elseOps {
if tOp.isWrite() {
return true
}
}
return false
}
return op.t != tRange
}
func NewOp() *Op {
return &Op{key: []byte("")}
}
// OpGet returns "get" operation based on given key and operation options.
func OpGet(key string, opts ...OpOption) Op {
// WithPrefix and WithFromKey are not supported together
if IsOptsWithPrefix(opts) && IsOptsWithFromKey(opts) {
panic("`WithPrefix` and `WithFromKey` cannot be set at the same time, choose one")
}
ret := Op{t: tRange, key: []byte(key)}
ret.applyOpts(opts)
return ret
}
// OpDelete returns "delete" operation based on given key and operation options.
func OpDelete(key string, opts ...OpOption) Op {
// WithPrefix and WithFromKey are not supported together
if IsOptsWithPrefix(opts) && IsOptsWithFromKey(opts) {
panic("`WithPrefix` and `WithFromKey` cannot be set at the same time, choose one")
}
ret := Op{t: tDeleteRange, key: []byte(key)}
ret.applyOpts(opts)
switch {
case ret.leaseID != 0:
panic("unexpected lease in delete")
case ret.limit != 0:View on GitHub (pinned to f744d457f4)
Solutions
- Choose one: WithPrefix() for all keys sharing the prefix, or WithFromKey() for all keys lexicographically >= key.
- In option-assembly code, use else-if / exclusive branches so the two can never both be appended.
- If you need 'prefix and beyond', that is exactly WithFromKey alone — drop WithPrefix.
- Add a unit assertion on your assembled []OpOption (IsOptsWithPrefix && IsOptsWithFromKey) before issuing the op.
Example fix
// before
opts := []clientv3.OpOption{clientv3.WithLimit(10)}
if strings.HasSuffix(key, "/") {
opts = append(opts, clientv3.WithPrefix())
}
if fromKey {
opts = append(opts, clientv3.WithFromKey()) // both set when fromKey && suffix '/'
go cli.Get(ctx, key, opts...) // panics
// after
if fromKey {
opts = append(opts, clientv3.WithFromKey())
} else if strings.HasSuffix(key, "/") {
opts = append(opts, clientv3.WithPrefix())
} Defensive patterns
Strategy: validation
Validate before calling
// clientv3 exposes the same predicates the library uses:
if clientv3.IsOptsWithPrefix(opts) && clientv3.IsOptsWithFromKey(opts) {
return errors.New("WithPrefix and WithFromKey are mutually exclusive")
}
resp, err := cli.Get(ctx, key, opts...) Prevention
- Build range options in an if/else chain so WithPrefix and WithFromKey can never both be appended.
- Remember the semantics: WithPrefix = subtree under prefix; WithFromKey = everything >= key.
- Validate assembled []OpOption with clientv3.IsOptsWithPrefix/IsOptsWithFromKey in generic helpers.
When it happens
Trigger: clientv3.OpGet("/foo", clientv3.WithPrefix(), clientv3.WithFromKey()); also indirect via cli.Get(ctx, "/foo", clientv3.WithPrefix(), clientv3.WithFromKey()) which calls OpGet internally; commonly happens when options are assembled into a slice and conditionally appended (e.g. WithPrefix added when key ends in '/', WithFromKey added elsewhere).
Common situations: A generic helper like getRange(key, prefixMode) that appends both options from different flags; refactoring WithFromKey usage into code that already had WithPrefix; behavior differences — WithFromKey ignores trailing '/' semantics that WithPrefix has, so teams combine them trying to get '>= key, matching spirit of prefix'.
Related errors
AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15).
Data as JSON: /api/errors/4f8dfa5c06644b13.
Report an issue: GitHub.