{"record":{"id":"a54a451cac457379","repo":"etcd-io/etcd","slug":"unexpected-serializable-in-put","errorCode":null,"errorMessage":"unexpected serializable in put","messagePattern":"unexpected serializable in put","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":314,"sourceCode":"\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 {\n\tclonedCmps := make([]Cmp, len(cmps))\n\tfor i := range cmps {","sourceCodeStart":296,"sourceCodeEnd":332,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L296-L332","documentation":"OpPut panics with 'unexpected serializable in put' when WithSerializable is applied to a put. Serializable reads control whether a Get can be served by a possibly-stale follower; writes must always go through quorum, so the flag is rejected for puts. The panic is a fail-fast guard inside OpPut executed at construction time.","triggerScenarios":"Calling clientv3.OpPut(key, val, clientv3.WithSerializable()). Real-world route: an options variable built for a Get (e.g. opts := []clientv3.OpOption{clientv3.WithSerializable()}) reused for a Put, or a kv.Put(ctx, key, val, opts...) style helper that forwards read options to OpPut.","commonSituations":"Teams standardizing on a shared OpOption slice for 'weak consistency' reads that is accidentally passed to writes; migrating code from clientv3.KV.Get (which accepts WithSerializable) to explicit Op building; version upgrades where helper signatures widened to accept variadic options.","solutions":["Remove WithSerializable from the put call; puts are always linearizable by definition","Separate read options from write options in shared helpers (two explicit slices or two functions)","If the flag arrived via a helper, add a lint/test that constructs every op combination your code builds","Check for other read-only flags in the same options slice since sibling panics will fire next"],"exampleFix":"// before\nreadOpts := []clientv3.OpOption{clientv3.WithSerializable()}\nop := clientv3.OpPut(\"k\", \"v\", readOpts...)\n\n// after\nreadOpts := []clientv3.OpOption{clientv3.WithSerializable()}\nop := clientv3.OpGet(\"k\", readOpts...)\nputOp := clientv3.OpPut(\"k\", \"v\")","handlingStrategy":"validation","validationCode":"// Keep serializable scoped to reads: two explicit builders, never one shared slice.\nfunc readOpts() []clientv3.OpOption {\n\treturn []clientv3.OpOption{clientv3.WithSerializable()} // Get only\n}\nfunc writeOpts() []clientv3.OpOption {\n\treturn nil // puts take no consistency flags\n}","typeGuard":null,"tryCatchPattern":"defer func() {\n\tif r := recover(); r != nil {\n\t\tif msg, ok := r.(string); ok && strings.Contains(msg, \"in put\") {\n\t\t\tlog.Printf(\"misconfigured put options: %s\", msg)\n\t\t}\n\t}\n}()","preventionTips":["Treat WithSerializable as read-only tuning; never blanket-apply it","Review central option factories for flags applied to every call","Document which options are valid per operation type in your helpers"],"tags":["etcd","clientv3","panic","api-misuse","put","serializable"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}