XTLS/Xray-core · error

outbound metadata not found

Error message

outbound metadata not found

What it means

Internal invariant failure in Portal.HandleConnection: it pops the last element of the session's outbound chain (OutboundsFromContext) to inspect the connection target, and that element is nil. This should not happen in normal flows because every dispatched connection carries at least one outbound record; encountering it means the caller built a context without proper outbound metadata.

Source

Thrown at app/reverse/portal.go:71

	}, nil
}

func (p *Portal) Start() error {
	return p.ohm.AddHandler(context.Background(), &Outbound{
		portal: p,
		tag:    p.tag,
	})
}

func (p *Portal) Close() error {
	return p.ohm.RemoveHandler(context.Background(), p.tag)
}

func (p *Portal) HandleConnection(ctx context.Context, link *transport.Link) error {
	outbounds := session.OutboundsFromContext(ctx)
	ob := outbounds[len(outbounds)-1]
	if ob == nil {
		return errors.New("outbound metadata not found").AtError()
	}

	if isDomain(ob.Target, p.domain) {
		muxClient, err := mux.NewClientWorker(*link, mux.ClientStrategy{})
		if err != nil {
			return errors.New("failed to create mux client worker").Base(err).AtWarning()
		}

		worker, err := NewPortalWorker(muxClient)
		if err != nil {
			return errors.New("failed to create portal worker").Base(err)
		}

		p.picker.AddWorker(worker)

		if _, ok := link.Reader.(*pipe.Reader); !ok {
			select {
			case <-ctx.Done():

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Audit the custom dispatcher: ensure every Dispatch call carries a context built with session.ContextWithOutbounds and a fully populated, non-nil last outbound (Target set).
  2. Reproduce with loglevel debug and print the outbound chain length before dispatching to the portal tag.
  3. If using stock xray, update to the latest core release in case a regression introduced a path skipping outbound append.

Example fix

// before (custom integration)
ctx := session.ContextWithOutbounds(ctx, []*session.Outbound{nil}) // -> hits invariant

// after
ob := &session.Outbound{ Target: net.TCPDestination(net.DomainAddress("svc.reverse.internal"), 80) }
ctx := session.ContextWithOutbounds(ctx, []*session.Outbound{ob})
Defensive patterns

Strategy: type-guard

Validate before calling

// Build the context properly before dispatching to the portal tag
if len(session.OutboundsFromContext(ctx)) == 0 {
    ob := &session.Outbound{Target: dest}
    ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{ob})
}

Type guard

func hasTerminalOutbound(ctx context.Context) bool {
    obs := session.OutboundsFromContext(ctx)
    return len(obs) > 0 && obs[len(obs)-1] != nil
}

Try / catch

// Defensive guard at integration boundaries
if err := portal.HandleConnection(ctx, link); err != nil {
    if strings.Contains(err.Error(), "outbound metadata not found") {
        ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{Target: dest}})
        err = portal.HandleConnection(ctx, link)
    }
    return err
}

Prevention

When it happens

Trigger: Calling Portal.HandleConnection (directly or by dispatching to the portal's outbound tag) with a context that has no valid terminal session.Outbound — e.g. a custom integration constructing ctx via session.ContextWithOutbounds with a nil last element, or a code path that appended a nil outbound. The nil check fires before any tunnel logic runs.

Common situations: Almost exclusively a bug in custom code embedding xray-core that dispatches to the reverse portal tag with a malformed session context; stock configs never produce it. Also possible after refactors that changed where outbounds are appended.

Related errors


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