etcd-io/etcd · error

unexpected countOnly in put

Error message

unexpected countOnly in put

What it means

OpPut panics with 'unexpected countOnly in put' when WithCountOnly is applied to a put. CountOnly asks a range query to return only the number of keys, which has no meaning for a write, so OpPut rejects it at construction time with a panic instead of ignoring it. This is one of a family of guards that validate that only put-compatible options reach a put.

Source

Thrown at client/v3/op.go:316

}

// 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()
	}

View on GitHub (pinned to f744d457f4)

Solutions

  1. Remove WithCountOnly from the put call
  2. In existence-check-then-write code, build fresh options per operation instead of sharing one slice
  3. For conditional writes, prefer a Txn with If(CreateRevision(key) = 0) over count-then-put anyway
  4. Add unit tests over your option-building helpers so invalid combos panic in CI, not production

Example fix

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

// after
op := clientv3.OpPut("k", "v")
Defensive patterns

Strategy: validation

Validate before calling

// Count-then-act code: build separate ops instead of reusing the count options.
countOp := clientv3.OpGet("prefix/", clientv3.WithPrefix(), clientv3.WithCountOnly())
putOp := clientv3.OpPut("k", "v") // no count flag ever

Prevention

When it happens

Trigger: Calling clientv3.OpPut(key, val, clientv3.WithCountOnly()), typically because an options list assembled for a Count-style Get (e.g. WithCountOnly + WithRange) was pasted or programmatically reused for a Put.

Common situations: Existence-check helpers that count keys and then conditionally write, where the option slice is shared between both branches; generic CRUD wrappers accepting OpOption variadics; refactoring a Get+Put pair into a transaction without trimming options.

Related errors


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