{"record":{"id":"760ae92c70c98d1f","repo":"etcd-io/etcd","slug":"unexpected-creatednotify-in-put","errorCode":null,"errorMessage":"unexpected createdNotify in put","messagePattern":"unexpected createdNotify in put","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":324,"sourceCode":"\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 {\n\t\tclonedCmps[i] = cmps[i].Clone()\n\t}\n\treturn Op{t: tTxn, cmps: clonedCmps, thenOps: thenOps, elseOps: elseOps}\n}\n\nfunc OpWatch(key string, opts ...OpOption) Op {\n\tret := Op{t: tRange, key: []byte(key)}\n\tret.applyOpts(opts)\n\tswitch {\n\tcase ret.leaseID != 0:","sourceCodeStart":306,"sourceCodeEnd":342,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L306-L342","documentation":"OpPut panics with 'unexpected createdNotify in put' when WithCreatedNotify is applied to a put. CreatedNotify configures a watch to emit a synthetic event when the watch is established, which is meaningless for writes; OpPut's validation switch panics immediately. Note WithCreatedNotify only affects ops converted to watch requests, never puts.","triggerScenarios":"Calling clientv3.OpPut(key, val, clientv3.WithCreatedNotify()). Happens when watch-construction code (Watcher.Watch with WithCreateNotify) shares an option list with put construction, or when a generic 'do anything' helper forwards all options to every op type.","commonSituations":"Config-driven operation builders where a single options map feeds Get/Put/Watch alike; copy-paste from a clientv3.Watcher setup; refactors that unified option handling across operation types.","solutions":["Remove WithCreatedNotify from the put call","Scope watch options (WithCreatedNotify, WithFilterPut, WithFilterDelete, WithProgressNotify, WithPrevKV as appropriate) to watcher code only","Give puts their own narrow option builder that only accepts WithLease/WithIgnoreValue/WithIgnoreLease","Add unit coverage of option builders so misuse panics surface in tests"],"exampleFix":"// before\nop := clientv3.OpPut(\"k\", \"v\", clientv3.WithCreatedNotify())\n\n// after (watch keeps the option, put does not)\nwch := cli.Watch(ctx, \"k\", clientv3.WithCreatedNotify())\nop := clientv3.OpPut(\"k\", \"v\")","handlingStrategy":"validation","validationCode":"// Watch-notification options stay on the watch:\nwch := cli.Watch(ctx, \"k\", clientv3.WithCreatedNotify())\nop := clientv3.OpPut(\"k\", \"v\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Do not feed watch option lists into OpPut","Reject config-driven 'apply to everything' option maps","Test all constructed ops in unit tests"],"tags":["etcd","clientv3","panic","api-misuse","put","watch-notify"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}