{"record":{"id":"441e8729f714834f","repo":"etcd-io/etcd","slug":"unexpected-countonly-in-watch","errorCode":null,"errorMessage":"unexpected countOnly in watch","messagePattern":"unexpected countOnly in watch","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":351,"sourceCode":"\t\tclonedCmps[i] = cmps[i].Clone()\n\t}\n\treturn Op{t: tTxn, cmps: clonedCmps, thenOps: thenOps, elseOps: elseOps}\n}\n\nfunc OpWatch(key string, opts ...OpOption) Op {\n\tret := Op{t: tRange, key: []byte(key)}\n\tret.applyOpts(opts)\n\tswitch {\n\tcase ret.leaseID != 0:\n\t\tpanic(\"unexpected lease in watch\")\n\tcase ret.limit != 0:\n\t\tpanic(\"unexpected limit in watch\")\n\tcase ret.sort != nil:\n\t\tpanic(\"unexpected sort in watch\")\n\tcase ret.serializable:\n\t\tpanic(\"unexpected serializable in watch\")\n\tcase ret.countOnly:\n\t\tpanic(\"unexpected countOnly in watch\")\n\tcase ret.minModRev != 0, ret.maxModRev != 0:\n\t\tpanic(\"unexpected mod revision filter in watch\")\n\tcase ret.minCreateRev != 0, ret.maxCreateRev != 0:\n\t\tpanic(\"unexpected create revision filter in watch\")\n\t}\n\treturn ret\n}\n\nfunc (op *Op) applyOpts(opts []OpOption) {\n\tfor _, opt := range opts {\n\t\topt(op)\n\t}\n}\n\n// OpOption configures Operations like Get, Put, Delete.\ntype OpOption func(*Op)\n\n// WithLease attaches a lease ID to a key in 'Put' request.","sourceCodeStart":333,"sourceCodeEnd":369,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L333-L369","documentation":"OpWatch panics with 'unexpected countOnly in watch' when WithCountOnly is applied to a watch. CountOnly collapses a range response to just the count of matching keys; a watch returns an event stream, so a count-only mode is rejected. OpWatch validates its option set and panics at construction time.","triggerScenarios":"Calling clientv3.OpWatch(key, clientv3.WithCountOnly()); reusing options from an existence/count check (WithCountOnly + WithPrefix) when subscribing to the same prefix; generic wrappers that forward a user's entire option list into both counts and watches.","commonSituations":"Health-check or quota code that counts prefix members and then watches the prefix for changes; shared option slices across monitoring paths; refactors moving from poll-count to watch without pruning options.","solutions":["Remove WithCountOnly from the watch call","If you need both a current count and change notifications, do a count Get plus a plain watch as two separate ops","Build watch options explicitly (WithPrefix, WithPrevKV, ...) rather than reusing read options","Unit-test every op your binary constructs; these panics are cheap to catch in CI"],"exampleFix":"// before\nop := clientv3.OpWatch(\"prefix/\", clientv3.WithPrefix(), clientv3.WithCountOnly())\n\n// after\ncntOp := clientv3.OpGet(\"prefix/\", clientv3.WithPrefix(), clientv3.WithCountOnly())\nwop := clientv3.OpWatch(\"prefix/\", clientv3.WithPrefix())","handlingStrategy":"validation","validationCode":"// Count and watch are two distinct ops:\ncnt := clientv3.OpGet(\"p/\", clientv3.WithPrefix(), clientv3.WithCountOnly())\nwop := clientv3.OpWatch(\"p/\", clientv3.WithPrefix())","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep count flags out of subscriptions","Recompute counts from watch events if needed","Test watch construction paths"],"tags":["etcd","clientv3","panic","api-misuse","watch","count-only"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}