{"record":{"id":"7b9f0a7f64bd0e2f","repo":"hashicorp/consul","slug":"unknown-placement-d","errorCode":null,"errorMessage":"unknown placement %d","messagePattern":"unknown placement (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/controller/controller.go","lineNumber":301,"sourceCode":"\tPlacementSingleton Placement = iota\n\n\t// PlacementEachServer ensures there is a replica of the controller running on\n\t// each server in the cluster. It is useful for cases where the controller is\n\t// responsible for applying some configuration resource to the server whenever\n\t// it changes (e.g. rate-limit configuration). Generally, controllers in this\n\t// placement mode should not modify resources.\n\tPlacementEachServer\n)\n\n// String satisfies the fmt.Stringer interface.\nfunc (p Placement) String() string {\n\tswitch p {\n\tcase PlacementSingleton:\n\t\treturn \"singleton\"\n\tcase PlacementEachServer:\n\t\treturn \"each-server\"\n\t}\n\tpanic(fmt.Sprintf(\"unknown placement %d\", p))\n}\n\n// Reconciler implements the business logic of a controller.\ntype Reconciler interface {\n\t// Reconcile the resource identified by req.ID.\n\tReconcile(ctx context.Context, rt Runtime, req Request) error\n}\n\n// RequeueAfterError is an error that allows a Reconciler to override the\n// exponential backoff behavior of the Controller, rather than applying\n// the backoff algorithm, returning a RequeueAfterError will cause the\n// Controller to reschedule the Request at a given time in the future.\ntype RequeueAfterError time.Duration\n\n// Error implements the error interface.\nfunc (r RequeueAfterError) Error() string {\n\treturn fmt.Sprintf(\"requeue at %s\", time.Duration(r))\n}","sourceCodeStart":283,"sourceCodeEnd":319,"githubUrl":"https://github.com/hashicorp/consul/blob/2397ff0d763d34f2fe37fe59fde6a7f7fc430a3e/internal/controller/controller.go#L283-L319","documentation":"Placement.String() (internal/controller/controller.go:294) panics for any Placement value other than PlacementSingleton (0) and PlacementEachServer (1). The panic fires only when the value is formatted (fmt printing, logging, error messages), because Go fmt calls String(). Note WithPlacement performs no validation, so an out-of-range value can sit dormant on the Controller until something prints it.","triggerScenarios":"Constructing a Placement from an unvalidated int, e.g. controller.Placement(cfgValue) where cfgValue is 2 or negative; deserializing a Placement from config/RPC data written by a newer binary that added a third placement mode, then logging or formatting it.","commonSituations":"Version skew between mixed Consul versions where a newer placement constant reaches older code; hand-written config files with a numeric placement field; forks that add a placement mode but forget to extend String().","solutions":["Validate the raw int is 0 or 1 before converting to Placement, and reject with an error at the decode boundary","Upgrade all servers/agents in mixed-version clusters so every binary knows the same placement values","If you forked and added a placement mode, add its case to Placement.String()"],"exampleFix":"// before\np := controller.Placement(cfg.GetInt(\"placement\")) // cfg value = 2\nfmt.Println(\"placement:\", p) // String() panics: unknown placement 2\n\n// after\nv := cfg.GetInt(\"placement\")\nif v != int(controller.PlacementSingleton) && v != int(controller.PlacementEachServer) {\n\treturn fmt.Errorf(\"invalid placement %d\", v)\n}\np := controller.Placement(v)","handlingStrategy":"validation","validationCode":"func parsePlacement(v int) (controller.Placement, error) {\n\tswitch controller.Placement(v) {\n\tcase controller.PlacementSingleton, controller.PlacementEachServer:\n\t\treturn controller.Placement(v), nil\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"unknown placement %d\", v)\n\t}\n}","typeGuard":"// Go narrowing helper: only passes known values through\nfunc validPlacement(p controller.Placement) bool {\n\tswitch p {\n\tcase controller.PlacementSingleton, controller.PlacementEachServer:\n\t\treturn true\n\t}\n\treturn false\n}","tryCatchPattern":"defer func() {\n\tif r := recover(); r != nil {\n\t\treturn fmt.Errorf(\"placement rejected: %v\", r)\n\t}\n}() // only worthwhile at a decode boundary; prefer validating the int before conversion","preventionTips":["Validate any integer crossing a serialization/config boundary before casting to a typed enum","Never print a Placement that came from external data without validation (String() is the panic site)","When adding enum values, keep String() exhaustive and consider a defaulting migration for old data"],"tags":["go","consul","controller","panic","enum","validation","version-skew"],"backgroundTag":null,"analyzedSha":"2397ff0d763d34f2fe37fe59fde6a7f7fc430a3e","analyzedAt":"2026-08-15T19:19:47.700Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}