fatedier/frp · error

feature gate %q with different spec already exists: %v

Error message

feature gate %q with different spec already exists: %v

What it means

Add() found the same feature name already registered with a different FeatureSpec. Re-adding an identical spec is an idempotent no-op (continue), but any difference in Default/LockToDefault/PreRelease is rejected to prevent two owners from disagreeing about a gate's contract.

Source

Thrown at pkg/policy/featuregate/feature_gate.go:150

// Add adds features to the feature gate
func (f *featureGate) Add(features map[Feature]FeatureSpec) error {
	f.lock.Lock()
	defer f.lock.Unlock()

	if f.closed {
		return fmt.Errorf("cannot add feature gates after the feature gate is closed")
	}

	// Copy existing state
	known := maps.Clone(f.known.Load().(map[Feature]FeatureSpec))

	// Add new features
	for name, spec := range features {
		if existingSpec, found := known[name]; found {
			if existingSpec == spec {
				continue
			}
			return fmt.Errorf("feature gate %q with different spec already exists: %v", name, existingSpec)
		}
		known[name] = spec
	}

	// Persist changes
	f.known.Store(known)

	return nil
}

// String returns a string containing all enabled feature gates, formatted as "key1=value1,key2=value2,..."
func (f *featureGate) String() string {
	enabled := f.enabled.Load().(map[Feature]bool)
	pairs := make([]string, 0, len(enabled))
	for k, v := range enabled {
		pairs = append(pairs, fmt.Sprintf("%s=%t", k, v))
	}
	sort.Strings(pairs)

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Make all Add sites register the exact same FeatureSpec for a given name (single source of truth)
  2. Consolidate registration into one package/const block and have others reference it
  3. If the spec intentionally changed, remove the old registration or use a new feature name

Example fix

// before
fg.Add(map[Feature]FeatureSpec{"F": {Default: false}})
fg.Add(map[Feature]FeatureSpec{"F": {Default: true}}) // error: different spec

// after — one canonical spec
var fSpec = FeatureSpec{DefaultToEnable: true}
fg.Add(map[Feature]FeatureSpec{"F": fSpec})
fg.Add(map[Feature]FeatureSpec{"F": fSpec}) // idempotent, ok
Defensive patterns

Strategy: validation

Validate before calling

// Register from a single shared spec map only
var allGates = map[Feature]FeatureSpec{
    "MyFeature": {DefaultToEnable: false, LockToDefault: false},
}
if err := fg.Add(allGates); err != nil { return err }

Try / catch

if err := fg.Add(specs); err != nil && strings.Contains(err.Error(), "different spec already exists") {
    // two owners disagree; reconcile specs to a single source of truth
    return err
}

Prevention

When it happens

Trigger: Two calls to Add with the same Feature name but differing FeatureSpec values — e.g. a module re-registers a shared gate with a different default after an upgrade.

Common situations: Two packages both try to own the same gate name with different specs; copy-pasted registration with an edited default; partially upgraded dependencies registering a revised spec for an existing gate.

Related errors


AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15). Data as JSON: /api/errors/4cc04221f482dc64. Report an issue: GitHub.