{"record":{"id":"f384af15652505d5","repo":"etcd-io/etcd","slug":"unexpected-filter-in-put","errorCode":null,"errorMessage":"unexpected filter in put","messagePattern":"unexpected filter in put","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":322,"sourceCode":"\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}\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)","sourceCodeStart":304,"sourceCodeEnd":340,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L304-L340","documentation":"OpPut panics with 'unexpected filter in put' when WithFilterDelete or WithFilterPut is applied to a put. These filters tell a range/watch to drop deletion or put events from results and only make sense on queries; OpPut rejects them at construction time. The shared message 'unexpected filter in put' covers both filter flags.","triggerScenarios":"Calling clientv3.OpPut(key, val, clientv3.WithFilterDelete()) or WithFilterPut(). Typically the options were assembled for a watch or a Get over historical events and reused for a write, e.g. replaying an event log and persisting state with the same option slice.","commonSituations":"Watch-event processors that read with event filters and then write derived state; option slices shared between watch setup and put; migrating from KV.Get to explicit Op construction while keeping old options.","solutions":["Remove WithFilterDelete/WithFilterPut from the put call","Build the write op with zero options (a plain OpPut(key, val)) unless you specifically need WithLease or WithPrevKV, which are the put-valid ones","Isolate watch/query option assembly from write option assembly in your codebase","Check the panic family list in client/v3/op.go OpPut to see the full set of invalid options"],"exampleFix":"// before\nop := clientv3.OpPut(\"k\", \"v\", clientv3.WithFilterDelete(), clientv3.WithFilterPut())\n\n// after\nop := clientv3.OpPut(\"k\", \"v\")","handlingStrategy":"validation","validationCode":"// Event filters are watch-only; keep them next to the Watch call.\nwch := cli.Watch(ctx, \"k\", clientv3.WithFilterDelete(), clientv3.WithFilterPut())\nop := clientv3.OpPut(\"k\", \"v\") // plain put, no filters","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Scope WithFilter* to watcher setup code","Write ops need at most WithLease/WithPrevKV/WithIgnore*","Lint against shared variadic option forwarding"],"tags":["etcd","clientv3","panic","api-misuse","put","watch-filter"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}