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
- Remove WithCountOnly from the put call
- In existence-check-then-write code, build fresh options per operation instead of sharing one slice
- For conditional writes, prefer a Txn with If(CreateRevision(key) = 0) over count-then-put anyway
- 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
- Never forward count/query options into writes
- Prefer Txn with If(CreateRevision(k) = 0) over count-then-put for existence checks
- Test option-building helpers so invalid combos panic in CI
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
- unexpected sort in put
- unexpected serializable in put
- unexpected mod revision filter in put
- unexpected create revision filter in put
- unexpected filter in put
AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15).
Data as JSON: /api/errors/8f4877df26bcde0c.
Report an issue: GitHub.