etcd-io/etcd · error
unexpected sort in put
Error message
unexpected sort in put
What it means
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.
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
Example fix
// before
op := clientv3.OpPut("foo", "bar", clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend))
// after
op := clientv3.OpPut("foo", "bar") Defensive patterns
Strategy: validation
Validate before calling
// Only put-valid options may reach OpPut: WithLease, WithPrevKV, WithIgnoreValue, WithIgnoreLease.
// WithSort is range-only; validate option lists at build time.
func buildPutOpts(opts []clientv3.OpOption) []clientv3.OpOption {
// put-valid options have zero effect on sort/limit/rev fields;
// simplest correct check: construct and recover
func() (ok bool) {
defer func() { _ = recover() }()
clientv3.OpPut("\x00probe", "\x00", opts...)
return true
}()
return opts
} Try / catch
// Go: panics are recoverable; use sparingly for third-party option lists
func safeOpPut(key, val string, opts ...clientv3.OpOption) (op clientv3.Op, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("invalid put options: %v", r)
}
}()
return clientv3.OpPut(key, val, opts...), nil
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- unexpected countOnly in put
- unexpected create revision filter in put
- unexpected createdNotify in put
- unexpected filter in put
- unexpected mod revision filter in put
AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15).
Data as JSON: /api/errors/f4682692184facb7.
Report an issue: GitHub.
Appendix: source
Thrown at client/v3/op.go:312
case ret.createdNotify:
panic("unexpected createdNotify in delete")
}
return ret
}
// OpPut returns "put" operation based on given key-value and operation options.
func OpPut(key, val string, opts ...OpOption) Op {
ret := Op{t: tPut, key: []byte(key), val: []byte(val)}
ret.applyOpts(opts)
switch {
case ret.end != nil:
panic("unexpected range in put")
case ret.limit != 0:
panic("unexpected limit in put")
case ret.rev != 0:
panic("unexpected revision in put")
case ret.sort != nil:
panic("unexpected sort in put")
case ret.serializable:
panic("unexpected serializable in put")
case ret.countOnly:
panic("unexpected countOnly in put")
case ret.minModRev != 0, ret.maxModRev != 0:
panic("unexpected mod revision filter in put")
case ret.minCreateRev != 0, ret.maxCreateRev != 0:
panic("unexpected create revision filter in put")
case ret.filterDelete, ret.filterPut:
panic("unexpected filter in put")
case ret.createdNotify:
panic("unexpected createdNotify in put")
}
return ret
}
// OpTxn returns "txn" operation based on given transaction conditions.
func OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op {View on GitHub (pinned to f744d457f4)