{"record":{"id":"1567d68cf8a84108","repo":"etcd-io/etcd","slug":"unexpected-create-revision-filter-in-watch","errorCode":null,"errorMessage":"unexpected create revision filter in watch","messagePattern":"unexpected create revision filter in watch","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":355,"sourceCode":"\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.\nfunc WithLease(leaseID LeaseID) OpOption {\n\treturn func(op *Op) { op.leaseID = leaseID }\n}\n","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L337-L373","documentation":"OpWatch panics with 'unexpected create revision filter in watch' when WithMinCreateRev or WithMaxCreateRev is applied to a watch. Create-revision filters narrow range results by when keys were created; watches have no such filtering, so OpWatch's validation switch rejects them with this panic before any stream opens.","triggerScenarios":"Calling clientv3.OpWatch(key, clientv3.WithMinCreateRev(n)) or WithMaxCreateRev(n); reusing options from a listing query that filters keys by creation time when subscribing to the same keys; unified option structs mapped onto every operation type.","commonSituations":"'Keys created since X' audit features that both list (filtered Get) and subscribe; copy-paste between adjacent listing and watching code; refactors that collapsed option handling into one helper.","solutions":["Remove WithMinCreateRev/WithMaxCreateRev from the watch call","Filter create revisions client-side on incoming watch events using Kv.CreateRevision, or seed state with the filtered Get first","Maintain distinct builders for listing options and watch options","Add tests constructing all watches used by the application to fail fast in CI"],"exampleFix":"// before\nop := clientv3.OpWatch(\"k\", clientv3.WithMinCreateRev(1000))\n\n// after\nwop := clientv3.OpWatch(\"k\", clientv3.WithPrefix())\n// filter in handler: if ev.Kv.CreateRevision >= 1000 { ... }","handlingStrategy":"validation","validationCode":"// Filter created-revision client-side in the event handler:\nfor ev := range wch {\n\tif ev.Kv.CreateRevision >= minCreateRev {\n\t\thandle(ev)\n\t}\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Create-revision filters are query-only","Apply such filters in watch handlers client-side","Keep listing options away from watch construction"],"tags":["etcd","clientv3","panic","api-misuse","watch","revision-filter"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}