XTLS/Xray-core · warning

unsupported GetPrincipleTarget

Error message

unsupported GetPrincipleTarget

What it means

Router.GetPrincipleTarget was called with a valid balancer tag, but the balancer's strategy does not implement the BalancingPrincipleTarget interface (the optional GetPrincipleTarget(candidates) method). Only strategies that expose their decision rationale — such as built-in leastload/leastping styles that implement it — can answer; random or custom strategies cannot, so the router returns this unsupported-capability error.

Source

Thrown at app/router/balancing.go:147

	hs, ok := b.ohm.(outbound.HandlerSelector)
	if !ok {
		return nil, errors.New("outbound.Manager is not a HandlerSelector")
	}
	tags := hs.Select(b.selectors)
	return tags, nil
}

// GetPrincipleTarget implements routing.BalancerPrincipleTarget
func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
	if b, ok := r.balancers[tag]; ok {
		if s, ok := b.strategy.(BalancingPrincipleTarget); ok {
			candidates, err := b.SelectOutbounds()
			if err != nil {
				return nil, errors.New("unable to select outbounds").Base(err)
			}
			return s.GetPrincipleTarget(candidates), nil
		}
		return nil, errors.New("unsupported GetPrincipleTarget")
	}
	return nil, errors.New("cannot find tag")
}

// SetOverrideTarget implements routing.BalancerOverrider
func (r *Router) SetOverrideTarget(tag, target string) error {
	if b, ok := r.balancers[tag]; ok {
		b.override.Put(target)
		return nil
	}
	return errors.New("cannot find tag")
}

// GetOverrideTarget implements routing.BalancerOverrider
func (r *Router) GetOverrideTarget(tag string) (string, error) {
	if b, ok := r.balancers[tag]; ok {
		return b.override.Get(), nil
	}

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Switch the balancer's strategy to one implementing principle-target reporting (e.g. leastPing/leastLoad) if the observation feature is needed.
  2. Keep random balancers but have tooling expect and skip this error (capability probe pattern).
  3. No forwarding impact — verify data path separately before spending time here.

Example fix

// before
"balancers": [ { "tag": "bal", "selector": [ "node" ], "strategy": { "type": "random" } } ]

// after
"balancers": [ { "tag": "bal", "selector": [ "node" ], "strategy": { "type": "leastPing" } } ]
Defensive patterns

Strategy: type-guard

Validate before calling

null // runtime capability depends on the configured strategy type

Type guard

func strategyReportsPrincipleTarget(strategyType string) bool {
    switch strategyType {
    case "leastPing", "leastLoad", "": return true // defaults implement it
    default: return false // e.g. random, custom
    }
}

Try / catch

if _, err := router.GetPrincipleTarget(tag); err != nil {
    if strings.Contains(err.Error(), "unsupported GetPrincipleTarget") {
        return nil, nil // capability gap, not an error condition for callers
    }
    return nil, err
}

Prevention

When it happens

Trigger: Calling the principle-target API for a balancer configured with "strategy" whose type is random (or a custom strategy) — the type assertion b.strategy.(BalancingPrincipleTarget) fails and the error is returned before any candidate work.

Common situations: Panels/tools that display 'current best node' querying every balancer, including ones using the random strategy; users switching a balancer from leastping to random and seeing the API query start failing while forwarding still works fine.

Related errors


AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15). Data as JSON: /api/errors/2799411805db527f. Report an issue: GitHub.