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
- Remove WithMinModRev/WithMaxModRev from the put call
- 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
- Split read vs write option slices in shared helpers
- 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
- Map 'write only if at revision N' to Compare(ModRevision), not WithMinModRev
- Keep revision-window filters on reads
- Split read/write option assembly
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
- unexpected create revision filter in put
- unexpected sort in put
- unexpected serializable in put
- unexpected countOnly in put
- unexpected filter in put
AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15).
Data as JSON: /api/errors/c6a2197487f7d747.
Report an issue: GitHub.