{"record":{"id":"318546736bc33f00","repo":"etcd-io/etcd","slug":"unexpected-revision-in-put","errorCode":null,"errorMessage":"unexpected revision in put","messagePattern":"unexpected revision in put","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":310,"sourceCode":"\tcase ret.filterDelete, ret.filterPut:\n\t\tpanic(\"unexpected filter in delete\")\n\tcase ret.createdNotify:\n\t\tpanic(\"unexpected createdNotify in delete\")\n\t}\n\treturn ret\n}\n\n// OpPut returns \"put\" operation based on given key-value and operation options.\nfunc OpPut(key, val string, opts ...OpOption) Op {\n\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","sourceCodeStart":292,"sourceCodeEnd":328,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L292-L328","documentation":"OpPut rejects WithRev(n). WithRev pins a range read to a historical MVCC revision; writes always apply to the latest state, so a revision-pinned put is contradictory. When applyOpts leaves ret.rev non-zero, the constructor's validation switch panics with \"unexpected revision in put\".","triggerScenarios":"clientv3.OpPut(\"/k\", \"v\", clientv3.WithRev(42)); read-modify-write flows that reuse the historical Get's options (WithRev for snapshot reads) on the write-back Put; watch-replay code carrying event revisions into re-puts.","commonSituations":"Optimistic-concurrency attempts: developers pin the Get to a revision and try to 'put at that revision' — the correct etcd primitive is a Txn If with CompareModified/CompareCreateRevision; option-slice reuse between snapshot reads and writes.","solutions":["Remove WithRev from Put calls.","For 'put only if unchanged since revision R', use cli.Txn(ctx).If(clientv3.Compare(clientv3.CompareModified(k), \"=\", R)).Then(clientv3.OpPut(k, v)).Commit() and check resp.Succeeded.","Keep snapshot-read options separate from write options."],"exampleFix":"// before\n_, err := cli.Put(ctx, k, newVal, clientv3.WithRev(seenRev)) // panics: unexpected revision in put\n\n// after — optimistic write guarded by the observed revision\nresp, err := cli.Txn(ctx).\n    If(clientv3.Compare(clientv3.CompareModified(k), \"=\", seenRev)).\n    Then(clientv3.OpPut(k, newVal)).\n    Commit()\nif !resp.Succeeded { /* concurrent modification: re-read and retry */ }","handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":["Writes always target latest state; WithRev is only for historical reads.","For optimistic concurrency use Txn If(Compare(CompareModified(k), \"=\", rev)) Then(OpPut(...)) and check resp.Succeeded.","Do not carry a snapshot Get's options into the write-back Put."],"tags":["go","etcd","clientv3","put","revision","mvcc","optimistic-concurrency","panic","invalid-option"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}