etcd-io/etcd · error

unexpected mod revision filter in put

Error message

unexpected mod revision filter in put

What it means

OpPut panics with 'unexpected mod revision filter in put' when a modification-revision filter (WithMinModRev or WithMaxModRev) is applied to a put. These filters restrict range results to keys modified within a revision window and are only valid on queries. OpPut validates its option set up front and panics on any query-only option.

Source

Thrown at client/v3/op.go:318

// OpPut returns "put" operation based on given key-value and operation options.
func OpPut(key, val string, opts ...OpOption) Op {
	ret := Op{t: tPut, key: []byte(key), val: []byte(val)}
	ret.applyOpts(opts)
	switch {
	case ret.end != nil:
		panic("unexpected range in put")
	case ret.limit != 0:
		panic("unexpected limit in put")
	case ret.rev != 0:
		panic("unexpected revision in put")
	case ret.sort != nil:
		panic("unexpected sort in put")
	case ret.serializable:
		panic("unexpected serializable in put")
	case ret.countOnly:
		panic("unexpected countOnly in put")
	case ret.minModRev != 0, ret.maxModRev != 0:
		panic("unexpected mod revision filter in put")
	case ret.minCreateRev != 0, ret.maxCreateRev != 0:
		panic("unexpected create revision filter in put")
	case ret.filterDelete, ret.filterPut:
		panic("unexpected filter in put")
	case ret.createdNotify:
		panic("unexpected createdNotify in put")
	}
	return ret
}

// OpTxn returns "txn" operation based on given transaction conditions.
func OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op {
	clonedCmps := make([]Cmp, len(cmps))
	for i := range cmps {
		clonedCmps[i] = cmps[i].Clone()
	}
	return Op{t: tTxn, cmps: clonedCmps, thenOps: thenOps, elseOps: elseOps}
}

View on GitHub (pinned to f744d457f4)

Solutions

  1. Remove WithMinModRev/WithMaxModRev from the put call
  2. If you intended optimistic concurrency (write only if the key is at revision N), use a Txn with If(ModRevision(key) = N) and the put in Then instead
  3. Split read vs write option slices in shared helpers
  4. Verify no other filter options (WithMinCreateRev, WithMaxCreateRev, WithFilter*) ride along in the same slice

Example fix

// before
op := clientv3.OpPut("k", "v", clientv3.WithMinModRev(42))

// after (optimistic write gated on revision)
resp, err := cli.Txn(ctx).
	If(clientv3.Compare(clientv3.ModRevision("k"), "=", 42)).
	Then(clientv3.OpPut("k", "v")).
	Commit()
Defensive patterns

Strategy: validation

Validate before calling

// If the intent is revision-gated write, use Compare instead of filters:
func putIfModRev(ctx context.Context, cli *clientv3.Client, k, v string, rev int64) (*clientv3.TxnResponse, error) {
	return cli.Txn(ctx).
		If(clientv3.Compare(clientv3.ModRevision(k), "=", rev)).
		Then(clientv3.OpPut(k, v)).
		Commit()
}

Prevention

When it happens

Trigger: Calling clientv3.OpPut(key, val, clientv3.WithMinModRev(n)) or WithMaxModRev(n). Commonly happens when options prepared for a filtered Get (e.g. watching history or incremental sync) are forwarded verbatim to a put in the same helper or code path.

Common situations: Historical/incremental sync utilities that read with revision windows and write back cached entries; helper functions taking a single variadic OpOption list used for both reads and writes; copy-paste between adjacent Get and Put calls.

Related errors


AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15). Data as JSON: /api/errors/c6a2197487f7d747. Report an issue: GitHub.