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

  1. Choose one: WithPrefix() for all keys sharing the prefix, or WithFromKey() for all keys lexicographically >= key.
  2. In option-assembly code, use else-if / exclusive branches so the two can never both be appended.
  3. If you need 'prefix and beyond', that is exactly WithFromKey alone — drop WithPrefix.
  4. 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

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.