{"record":{"id":"f4682692184facb7","repo":"etcd-io/etcd","slug":"unexpected-sort-in-put","errorCode":null,"errorMessage":"unexpected sort in put","messagePattern":"unexpected sort in put","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":312,"sourceCode":"\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\n// OpTxn returns \"txn\" operation based on given transaction conditions.\nfunc OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op {","sourceCodeStart":294,"sourceCodeEnd":330,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L294-L330","documentation":"OpPut panics with 'unexpected sort in put' when a sort option (WithSort) is applied to a put operation. etcd's clientv3 Op constructors fail fast: options that only make sense for range (Get) queries are rejected at construction time rather than silently ignored. The panic happens synchronously inside OpPut, before any RPC is issued.","triggerScenarios":"Calling clientv3.OpPut(key, val, clientv3.WithSort(...)) or WithSortBytes/WithSortKey/WithSortRev/WithSortVersion/WithSortCreateRev/WithSortModRev/WithSortValue. Also triggered indirectly by building a Put through clientv3.Txn Then/Else branches with a sort option attached, or by reusing an options slice built for a Range query in a Put.","commonSituations":"Copy-pasting an options list from a Get call into a Put call; generic helper functions that accept variadic OpOption and forward the same set to both OpGet and OpPut; refactoring a range into a put without removing WithSort.","solutions":["Remove the WithSort* option from the OpPut call; sorting is meaningless for writes","If the options come from a shared helper, split the option set: keep sort/limit/serializable options only on the OpGet path","Audit the call site for other range-only options (WithLimit, WithRev, WithRange, WithSerializable, WithCountOnly, WithPrefix filters) since the same panic family guards them","Wrap Op construction in a recover() only if you must tolerate third-party option lists"],"exampleFix":"// before\nop := clientv3.OpPut(\"foo\", \"bar\", clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend))\n\n// after\nop := clientv3.OpPut(\"foo\", \"bar\")","handlingStrategy":"validation","validationCode":"// Only put-valid options may reach OpPut: WithLease, WithPrevKV, WithIgnoreValue, WithIgnoreLease.\n// WithSort is range-only; validate option lists at build time.\nfunc buildPutOpts(opts []clientv3.OpOption) []clientv3.OpOption {\n\t// put-valid options have zero effect on sort/limit/rev fields;\n\t// simplest correct check: construct and recover\n\tfunc() (ok bool) {\n\t\tdefer func() { _ = recover() }()\n\t\tclientv3.OpPut(\"\\x00probe\", \"\\x00\", opts...)\n\t\treturn true\n\t}()\n\treturn opts\n}","typeGuard":null,"tryCatchPattern":"// Go: panics are recoverable; use sparingly for third-party option lists\nfunc safeOpPut(key, val string, opts ...clientv3.OpOption) (op clientv3.Op, err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\terr = fmt.Errorf(\"invalid put options: %v\", r)\n\t\t}\n\t}()\n\treturn clientv3.OpPut(key, val, opts...), nil\n}","preventionTips":["Never share one OpOption slice between Get and Put call sites","Keep WithSort/WithLimit/WithSerializable confined to read paths","Unit-test every option combination your code constructs"],"tags":["etcd","clientv3","panic","api-misuse","put","sort"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}