hashicorp/consul · error

unknown placement %d

Error message

unknown placement %d

What it means

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.

Source

Thrown at internal/controller/controller.go:301

	PlacementSingleton Placement = iota

	// PlacementEachServer ensures there is a replica of the controller running on
	// each server in the cluster. It is useful for cases where the controller is
	// responsible for applying some configuration resource to the server whenever
	// it changes (e.g. rate-limit configuration). Generally, controllers in this
	// placement mode should not modify resources.
	PlacementEachServer
)

// String satisfies the fmt.Stringer interface.
func (p Placement) String() string {
	switch p {
	case PlacementSingleton:
		return "singleton"
	case PlacementEachServer:
		return "each-server"
	}
	panic(fmt.Sprintf("unknown placement %d", p))
}

// Reconciler implements the business logic of a controller.
type Reconciler interface {
	// Reconcile the resource identified by req.ID.
	Reconcile(ctx context.Context, rt Runtime, req Request) error
}

// RequeueAfterError is an error that allows a Reconciler to override the
// exponential backoff behavior of the Controller, rather than applying
// the backoff algorithm, returning a RequeueAfterError will cause the
// Controller to reschedule the Request at a given time in the future.
type RequeueAfterError time.Duration

// Error implements the error interface.
func (r RequeueAfterError) Error() string {
	return fmt.Sprintf("requeue at %s", time.Duration(r))
}

View on GitHub (pinned to 2397ff0d76)

Solutions

  1. Validate the raw int is 0 or 1 before converting to Placement, and reject with an error at the decode boundary
  2. Upgrade all servers/agents in mixed-version clusters so every binary knows the same placement values
  3. If you forked and added a placement mode, add its case to Placement.String()

Example fix

// before
p := controller.Placement(cfg.GetInt("placement")) // cfg value = 2
fmt.Println("placement:", p) // String() panics: unknown placement 2

// after
v := cfg.GetInt("placement")
if v != int(controller.PlacementSingleton) && v != int(controller.PlacementEachServer) {
	return fmt.Errorf("invalid placement %d", v)
}
p := controller.Placement(v)
Defensive patterns

Strategy: validation

Validate before calling

func parsePlacement(v int) (controller.Placement, error) {
	switch controller.Placement(v) {
	case controller.PlacementSingleton, controller.PlacementEachServer:
		return controller.Placement(v), nil
	default:
		return 0, fmt.Errorf("unknown placement %d", v)
	}
}

Type guard

// Go narrowing helper: only passes known values through
func validPlacement(p controller.Placement) bool {
	switch p {
	case controller.PlacementSingleton, controller.PlacementEachServer:
		return true
	}
	return false
}

Try / catch

defer func() {
	if r := recover(); r != nil {
		return fmt.Errorf("placement rejected: %v", r)
	}
}() // only worthwhile at a decode boundary; prefer validating the int before conversion

Prevention

When it happens

Trigger: 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.

Common situations: 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().

Related errors


AI-assisted analysis of hashicorp/consul@2397ff0d76 (2026-08-15). Data as JSON: /api/errors/7b9f0a7f64bd0e2f. Report an issue: GitHub.