{"record":{"id":"d5b886deda5a2591","repo":"etcd-io/etcd","slug":"unexpected-revision-in-delete","errorCode":null,"errorMessage":"unexpected revision in delete","messagePattern":"unexpected revision in delete","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":281,"sourceCode":"\tret.applyOpts(opts)\n\treturn ret\n}\n\n// OpDelete returns \"delete\" operation based on given key and operation options.\nfunc OpDelete(key string, opts ...OpOption) Op {\n\t// WithPrefix and WithFromKey are not supported together\n\tif IsOptsWithPrefix(opts) && IsOptsWithFromKey(opts) {\n\t\tpanic(\"`WithPrefix` and `WithFromKey` cannot be set at the same time, choose one\")\n\t}\n\tret := Op{t: tDeleteRange, key: []byte(key)}\n\tret.applyOpts(opts)\n\tswitch {\n\tcase ret.leaseID != 0:\n\t\tpanic(\"unexpected lease in delete\")\n\tcase ret.limit != 0:\n\t\tpanic(\"unexpected limit in delete\")\n\tcase ret.rev != 0:\n\t\tpanic(\"unexpected revision in delete\")\n\tcase ret.sort != nil:\n\t\tpanic(\"unexpected sort in delete\")\n\tcase ret.serializable:\n\t\tpanic(\"unexpected serializable in delete\")\n\tcase ret.countOnly:\n\t\tpanic(\"unexpected countOnly in delete\")\n\tcase ret.minModRev != 0, ret.maxModRev != 0:\n\t\tpanic(\"unexpected mod revision filter in delete\")\n\tcase ret.minCreateRev != 0, ret.maxCreateRev != 0:\n\t\tpanic(\"unexpected create revision filter in delete\")\n\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","sourceCodeStart":263,"sourceCodeEnd":299,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L263-L299","documentation":"OpDelete rejects WithRev(n). WithRev pins a range read to a historical revision (MVCC snapshot); deletes always operate on the latest state, so a revision-pinned delete is meaningless. applyOpts records ret.rev, and the post-condition switch panics with \"unexpected revision in delete\" when it is non-zero.","triggerScenarios":"clientv3.OpDelete(k, clientv3.WithRev(1234)); reusing an option slice that was built for a consistent historical Get; 'delete if it looked like X at revision R' attempts — that belongs to a Txn If-guard, not WithRev.","commonSituations":"Read-modify-delete flows where the Get used WithRev and the options object is reused; code imported from a watch-replay routine that pins revisions; conflating Txn If CompareModified with WithRev.","solutions":["Remove WithRev from Delete; if you need conditional deletion, use cli.Txn(...).If(clientv3.Compare(clientv3.CompareModified(k), \"=\", rev)).Then(clientv3.OpDelete(k)).","Never share option slices between the Get and the Delete of a read-modify-write cycle."],"exampleFix":"// before\n_, err := cli.Delete(ctx, k, clientv3.WithRev(seenRev)) // panics\n\n// after — conditional delete on the observed revision\nresp, err := cli.Txn(ctx).\n    If(clientv3.Compare(clientv3.CompareModified(k), \"=\", seenRev)).\n    Then(clientv3.OpDelete(k)).\n    Commit()","handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":["WithRev is a read-only (historical snapshot) option; it never applies to writes.","For revision-conditional deletes, use a Txn If with CompareModified/CompareCreated.","Never reuse a snapshot Get's option slice for the corresponding write."],"tags":["go","etcd","clientv3","delete","revision","mvcc","panic","invalid-option"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}