MHSanaei/3x-ui · error

inbound %d: %w

Error message

inbound %d: %w

What it means

From InboundService's multi-inbound client update/delete loop (client_crud.go): fetching one of the client's inboundIds via GetInbound failed with an error other than gorm.ErrRecordNotFound (which is skipped). The per-inbound error is collected with the inbound ID and later joined with errors.Join, so one bad inbound does not abort the rest but is reported.

Source

Thrown at internal/web/service/client_crud.go:538

	if err != nil {
		return false, err
	}
	tombstoneClientEmail(existing.Email)

	inboundIds, err := s.GetInboundIdsForRecord(id)
	if err != nil {
		withdrawClientTombstones(existing.Email)
		return false, err
	}

	needRestart := false
	var delErrs []error
	for _, ibId := range inboundIds {
		if _, getErr := inboundSvc.GetInbound(ibId); getErr != nil {
			if errors.Is(getErr, gorm.ErrRecordNotFound) {
				continue
			}
			delErrs = append(delErrs, fmt.Errorf("inbound %d: %w", ibId, getErr))
			continue
		}

		// Always delete by email — the client's stable identity. This removes
		// every matching entry from the inbound's settings even when the stored
		// credential (UUID/password/auth) drifted from the inbound JSON, or a
		// duplicate entry with the same email exists.
		if existing.Email == "" {
			continue
		}
		nr, delErr := s.DelInboundClientByEmail(inboundSvc, ibId, existing.Email, keepTraffic, true)
		if delErr != nil {
			// The client is already absent from this inbound (data drift or a
			// retried delete). Skip it — deletion stays idempotent.
			if errors.Is(delErr, ErrClientNotInInbound) {
				continue
			}
			delErrs = append(delErrs, fmt.Errorf("inbound %d: %w", ibId, delErr))

View on GitHub (pinned to ad32144c42)

Solutions

  1. Check the panel DB: for SQLite run the panel's built-in repair/restart, for Postgres verify connectivity and that the inbound table is readable.
  2. Look at the WRAPPED error after 'inbound N:' — it is the raw GORM/driver message and pinpoints the DB fault.
  3. Retry the client operation once DB health is restored; the loop is designed so leftovers are retried.
  4. If one specific inboundId always fails, inspect that row for corruption and remove the stale reference from the client's inboundIds.

Example fix

// before: err: inbound 7: database is locked (5) (SQLITE_BUSY)
// during concurrent panel writes

// after: serialize the operation (retry after the other write finishes) or move to PostgreSQL for concurrent multi-admin use
Defensive patterns

Strategy: retry

Validate before calling

// Pre-validate all referenced inbounds before the loop
for _, id := range clientInboundIds {
    if _, err := inboundSvc.GetInbound(id); err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
        return fmt.Errorf("db unhealthy for inbound %d: %w", id, err)
    }
}

Type guard

func isPerInboundError(err error) []error {
    if err == nil { return nil }
    if joined, ok := err.(interface{ Unwrap() []error }); ok {
        return joined.Unwrap()
    }
    return []error{err}
}

Try / catch

if _, err := s.UpdateClientTrafficAndStatus(...); err != nil {
    for _, e := range isPerInboundError(err) { // log each, retry leftovers later
        log.Printf("client op partial: %v", e)
    }
    if dbHealthy() { return retryOnce(op) }
    return err
}

Prevention

When it happens

Trigger: Updating/deleting a client whose client record references inboundIds where GetInbound returns a real DB error — connection lost to SQLite/Postgres, table locked, DB file corrupted, or a driver-level failure.

Common situations: Long-running panel operation raced a DB restart; SQLite 'database is locked' under concurrent writes; Postgres failover mid-operation; corrupted /etc/x-ui/x-ui.db after disk-full.

Related errors


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