XTLS/Xray-core · error

portal tag is empty

Error message

portal tag is empty

What it means

Reverse-proxy portal constructor validation: a Portal (the component on the public/outer side that accepts bridge connections and dispatches traffic into them) must have a tag, since the portal registers itself as an outbound handler under that tag so routing can send traffic to it. NewPortal fails fast when PortalConfig.Tag is empty.

Source

Thrown at app/reverse/portal.go:33

	"github.com/xtls/xray-core/common/signal"
	"github.com/xtls/xray-core/common/task"
	"github.com/xtls/xray-core/features/outbound"
	"github.com/xtls/xray-core/transport"
	"github.com/xtls/xray-core/transport/pipe"
	"google.golang.org/protobuf/proto"
)

type Portal struct {
	ohm    outbound.Manager
	tag    string
	domain string
	picker *StaticMuxPicker
	client *mux.ClientManager
}

func NewPortal(config *PortalConfig, ohm outbound.Manager) (*Portal, error) {
	if config.Tag == "" {
		return nil, errors.New("portal tag is empty")
	}

	if config.Domain == "" {
		return nil, errors.New("portal domain is empty")
	}

	picker, err := NewStaticMuxPicker()
	if err != nil {
		return nil, err
	}

	return &Portal{
		ohm:    ohm,
		tag:    config.Tag,
		domain: config.Domain,
		picker: picker,
		client: &mux.ClientManager{
			Picker: picker,

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Add a unique "tag" to the portals[] entry (e.g. 'portal').
  2. Make sure routing rules/balancers that should send traffic through the tunnel reference this exact tag.
  3. Run xray -test on the config to catch it before deployment.

Example fix

// before
"reverse": { "portals": [ { "domain": "svc.reverse.internal" } ] }

// after
"reverse": { "portals": [ { "tag": "portal", "domain": "svc.reverse.internal" } ] }
Defensive patterns

Strategy: validation

Validate before calling

for _, p := range cfg.Reverse.Portals {
    if p.Tag == "" { return errors.New("reverse config: portal tag is empty") }
}

Type guard

func validPortalConfig(p PortalConfig) bool { return p.Tag != "" }

Try / catch

if _, err := reverse.NewPortal(cfg, ohm); err != nil {
    if strings.Contains(err.Error(), "portal tag is empty") {
        return fmt.Errorf("reverse.portals[%d]: tag required", i)
    }
    return err
}

Prevention

When it happens

Trigger: A reverse config portals[] entry without a "tag" field during Portal construction; the portal later calls ohm.RemoveHandler with this same tag on Close, so an empty tag would corrupt outbound manager state.

Common situations: Hand-authored reverse tunnel configs missing the portal tag; field-name casing mistakes when hand-translating protobuf JSON; editing examples and removing the tag line. Instance startup fails immediately.

Related errors


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