{"record":{"id":"affcb6e2154ea9b3","repo":"etcd-io/etcd","slug":"unexpected-create-revision-filter-in-put","errorCode":null,"errorMessage":"unexpected create revision filter in put","messagePattern":"unexpected create revision filter in put","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":320,"sourceCode":"\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}\n\treturn Op{t: tTxn, cmps: clonedCmps, thenOps: thenOps, elseOps: elseOps}\n}\n\nfunc OpWatch(key string, opts ...OpOption) Op {","sourceCodeStart":302,"sourceCodeEnd":338,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L302-L338","documentation":"OpPut panics with 'unexpected create revision filter in put' when WithMinCreateRev or WithMaxCreateRev is applied to a put. Create-revision filters limit range results to keys created within a revision window; they are query-only and rejected by OpPut's validation switch. The panic fires synchronously during Op construction, before any network activity.","triggerScenarios":"Calling clientv3.OpPut(key, val, clientv3.WithMinCreateRev(n)) or WithMaxCreateRev(n); reusing an options slice built for a creation-filtered Get in a subsequent put; generic wrappers that merge user-supplied options into every operation type.","commonSituations":"Audit/listing code that filters keys by creation revision and later writes metadata back with the same options; copy-paste drift between Get and Put calls; refactors that moved option assembly into one shared function.","solutions":["Remove WithMinCreateRev/WithMaxCreateRev from the put","If the intent is 'write only if the key exists/doesn't exist', use a Txn with If(CreateRevision(key) ... ) instead","Keep option assembly per-operation-type: buildOptsGet() vs buildOptsPut()","Add a guard test that panics loudly in CI for every op your code constructs"],"exampleFix":"// before\nop := clientv3.OpPut(\"k\", \"v\", clientv3.WithMaxCreateRev(100))\n\n// after (write only if key already exists)\nresp, err := cli.Txn(ctx).\n\tIf(clientv3.Compare(clientv3.CreateRevision(\"k\"), \">\", 0)).\n\tThen(clientv3.OpPut(\"k\", \"v\")).\n\tCommit()","handlingStrategy":"validation","validationCode":"// Existence-gated write belongs in the Txn condition, not a filter option:\nresp, err := cli.Txn(ctx).\n\tIf(clientv3.Compare(clientv3.CreateRevision(\"k\"), \"=\", 0)). // key absent\n\tThen(clientv3.OpPut(\"k\", \"v\")).\n\tCommit()","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use CreateRevision/ModRevision Compare for conditional writes","Never pass With*CreateRev to OpPut","One option builder per operation type"],"tags":["etcd","clientv3","panic","api-misuse","put","revision-filter"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}