{"record":{"id":"035894a7e92a96e3","repo":"etcd-io/etcd","slug":"unexpected-mod-revision-filter-in-watch","errorCode":null,"errorMessage":"unexpected mod revision filter in watch","messagePattern":"unexpected mod revision filter in watch","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":353,"sourceCode":"\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.\nfunc WithLease(leaseID LeaseID) OpOption {\n\treturn func(op *Op) { op.leaseID = leaseID }","sourceCodeStart":335,"sourceCodeEnd":371,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L335-L371","documentation":"OpWatch panics with 'unexpected mod revision filter in watch' when WithMinModRev or WithMaxModRev is applied to a watch. Revision-window filters restrict range results to keys modified in a revision range; watches are configured with a start revision (WithRev), not filters, so OpWatch rejects these options. The panic occurs synchronously in OpWatch.","triggerScenarios":"Calling clientv3.OpWatch(key, clientv3.WithMinModRev(n)) or WithMaxModRev(n); converting a filtered historical read into a watch while keeping the filter options; generic sync helpers applying the same filters to reads and watches.","commonSituations":"Incremental-sync or backup tools that read a mod-revision window then watch for live changes; code sharing one options slice between the catch-up read and the subscription; version upgrades where option lists got merged during refactor.","solutions":["Remove WithMinModRev/WithMaxModRev from the watch call","To resume a watch from a point in history, use WithRev(startRevision) on the watch and optionally WithPrefix for ranges","Keep the revision-window filters on the catch-up Get only","Ensure option assembly for watch ops is a separate, minimal builder"],"exampleFix":"// before\nop := clientv3.OpWatch(\"k\", clientv3.WithMinModRev(500))\n\n// after\ncatchUp := clientv3.OpGet(\"k\", clientv3.WithMinModRev(500))\nwop := clientv3.OpWatch(\"k\", clientv3.WithRev(lastSeenRev+1))","handlingStrategy":"validation","validationCode":"// Resume watches with WithRev, not revision filters:\nwop := clientv3.OpWatch(\"k\", clientv3.WithRev(lastRev+1))","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use WithRev for historical resume on watches","Revision-window filters are read-only","Separate catch-up read options from watch options"],"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"}