{"record":{"id":"8f4877df26bcde0c","repo":"etcd-io/etcd","slug":"unexpected-countonly-in-put","errorCode":null,"errorMessage":"unexpected countOnly in put","messagePattern":"unexpected countOnly in put","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":316,"sourceCode":"}\n\n// OpPut returns \"put\" operation based on given key-value and operation options.\nfunc OpPut(key, val string, opts ...OpOption) Op {\n\tret := Op{t: tPut, key: []byte(key), val: []byte(val)}\n\tret.applyOpts(opts)\n\tswitch {\n\tcase ret.end != nil:\n\t\tpanic(\"unexpected range in put\")\n\tcase ret.limit != 0:\n\t\tpanic(\"unexpected limit in put\")\n\tcase ret.rev != 0:\n\t\tpanic(\"unexpected revision in put\")\n\tcase ret.sort != nil:\n\t\tpanic(\"unexpected sort in put\")\n\tcase ret.serializable:\n\t\tpanic(\"unexpected serializable in put\")\n\tcase ret.countOnly:\n\t\tpanic(\"unexpected countOnly in put\")\n\tcase ret.minModRev != 0, ret.maxModRev != 0:\n\t\tpanic(\"unexpected mod revision filter in put\")\n\tcase ret.minCreateRev != 0, ret.maxCreateRev != 0:\n\t\tpanic(\"unexpected create revision filter in put\")\n\tcase ret.filterDelete, ret.filterPut:\n\t\tpanic(\"unexpected filter in put\")\n\tcase ret.createdNotify:\n\t\tpanic(\"unexpected createdNotify in put\")\n\t}\n\treturn ret\n}\n\n// OpTxn returns \"txn\" operation based on given transaction conditions.\nfunc OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op {\n\tclonedCmps := make([]Cmp, len(cmps))\n\tfor i := range cmps {\n\t\tclonedCmps[i] = cmps[i].Clone()\n\t}","sourceCodeStart":298,"sourceCodeEnd":334,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L298-L334","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nop := clientv3.OpPut(\"k\", \"v\", clientv3.WithCountOnly())\n\n// after\nop := clientv3.OpPut(\"k\", \"v\")","handlingStrategy":"validation","validationCode":"// Count-then-act code: build separate ops instead of reusing the count options.\ncountOp := clientv3.OpGet(\"prefix/\", clientv3.WithPrefix(), clientv3.WithCountOnly())\nputOp := clientv3.OpPut(\"k\", \"v\") // no count flag ever","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["etcd","clientv3","panic","api-misuse","put","count-only"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}