MHSanaei/3x-ui · error

local xray is not running

Error message

local xray is not running

What it means

Thrown by the local runtime adapter (internal/web/runtime/local.go) inside withAPI. Before every gRPC call to the local Xray child process it reads the configured Xray API port; when APIPort() returns <= 0 there is no port to connect to, so the runtime refuses the call with 'local xray is not running'. It means the panel either has not started Xray yet or is running with no API listener configured, not that a connection attempt failed.

Source

Thrown at internal/web/runtime/local.go:38

type Local struct {
	deps LocalDeps
	mu   sync.Mutex
}

func NewLocal(deps LocalDeps) *Local {
	return &Local{deps: deps}
}

func (l *Local) Name() string { return "local" }

func (l *Local) withAPI(fn func(api *xray.XrayAPI) error) error {
	l.mu.Lock()
	defer l.mu.Unlock()

	port := l.deps.APIPort()
	if port <= 0 {
		return errors.New("local xray is not running")
	}
	var api xray.XrayAPI
	if err := api.Init(port); err != nil {
		return err
	}
	defer api.Close()
	return fn(&api)
}

func (l *Local) AddInbound(_ context.Context, ib *model.Inbound) error {
	if ib.Protocol == model.MTProto {
		inst, ok := mtproto.InstanceFromInbound(ib)
		if !ok {
			return nil
		}
		return mtproto.GetManager().Ensure(inst)
	}
	body, err := json.MarshalIndent(ib.GenXrayInboundConfig(), "", "  ")

View on GitHub (pinned to ad32144c42)

Solutions

  1. Verify the Xray child process is actually running (panel logs / xray process status) and restart it from the panel
  2. Check the Xray API port setting in the panel (must be > 0 and not collide with an inbound port)
  3. If this fires at startup, fix the initialization order so runtime-dependent jobs and handlers only run after xray is started
  4. In tests, make the fake LocalDeps.APIPort() return a positive value

Example fix

// before
port := l.deps.APIPort()
if port <= 0 {
    return errors.New("local xray is not running")
}

// after (caller side: start xray before dispatching)
if err := xray.StartXray(); err != nil { return err }
return runtime.AddInbound(ctx, ib)
Defensive patterns

Strategy: validation

Validate before calling

port := getAPIPortSetting() // same source LocalDeps.APIPort reads
if port <= 0 {
    return fmt.Errorf("cannot dispatch: xray API port unset; start xray first")
}
return runtime.AddInbound(ctx, ib)

Try / catch

if err := runtime.AddInbound(ctx, ib); err != nil {
    if strings.Contains(err.Error(), "local xray is not running") {
        // start/restart xray, then retry once
    }
}

Prevention

When it happens

Trigger: Any state-changing inbound/client operation dispatched through runtime.Runtime while the local Xray child process is down: calling InboundService add/update/del flows before xray.Start() succeeded, after a crash that has not been restarted (eventbus xray.crash), or when the xray API port setting is 0/unset so APIPort() returns 0.

Common situations: Panel startup ordering bugs (service jobs firing before Xray is up), Xray binary missing or failing to launch so the API port is never registered, a setting migration that wiped the apiPort value, or tests that construct LocalDeps without wiring APIPort.

Related errors


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/c6f01cfcbc9a4cd1. Report an issue: GitHub.