etcd-io/etcd · error

unexpected create revision filter in put

Error message

unexpected create revision filter in put

What it means

OpPut panics with 'unexpected create revision filter in put' when WithMinCreateRev or WithMaxCreateRev is applied to a put. Create-revision filters limit range results to keys created within a revision window; they are query-only and rejected by OpPut's validation switch. The panic fires synchronously during Op construction, before any network activity.

Source

Thrown at client/v3/op.go:320

	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}
}

func OpWatch(key string, opts ...OpOption) Op {

View on GitHub (pinned to f744d457f4)

Solutions

  1. Remove WithMinCreateRev/WithMaxCreateRev from the put
  2. If the intent is 'write only if the key exists/doesn't exist', use a Txn with If(CreateRevision(key) ... ) instead
  3. Keep option assembly per-operation-type: buildOptsGet() vs buildOptsPut()
  4. Add a guard test that panics loudly in CI for every op your code constructs

Example fix

// before
op := clientv3.OpPut("k", "v", clientv3.WithMaxCreateRev(100))

// after (write only if key already exists)
resp, err := cli.Txn(ctx).
	If(clientv3.Compare(clientv3.CreateRevision("k"), ">", 0)).
	Then(clientv3.OpPut("k", "v")).
	Commit()
Defensive patterns

Strategy: validation

Validate before calling

// Existence-gated write belongs in the Txn condition, not a filter option:
resp, err := cli.Txn(ctx).
	If(clientv3.Compare(clientv3.CreateRevision("k"), "=", 0)). // key absent
	Then(clientv3.OpPut("k", "v")).
	Commit()

Prevention

When it happens

Trigger: Calling clientv3.OpPut(key, val, clientv3.WithMinCreateRev(n)) or WithMaxCreateRev(n); reusing an options slice built for a creation-filtered Get in a subsequent put; generic wrappers that merge user-supplied options into every operation type.

Common situations: Audit/listing code that filters keys by creation revision and later writes metadata back with the same options; copy-paste drift between Get and Put calls; refactors that moved option assembly into one shared function.

Related errors


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