MHSanaei/3x-ui · error

remote DeleteUser: resolve tag %q: %w

Error message

remote DeleteUser: resolve tag %q: %w

What it means

Wraps the resolveRemoteID failure inside Remote.DeleteUser. By design the error is SURFACED rather than swallowed: because the panel cannot confirm the delete reached the node, returning an error makes the caller mark the node dirty so a later reconcile finishes the deletion — otherwise the next snapshot would resurrect the client.

Source

Thrown at internal/web/runtime/remote.go:566

		"client":     client,
		"inboundIds": []int{id},
	}
	if _, err := r.do(ctx, http.MethodPost, "panel/api/clients/add", payload); err != nil {
		return err
	}
	return nil
}

func (r *Remote) DeleteUser(ctx context.Context, ib *model.Inbound, email string) error {
	if email == "" {
		return nil
	}
	id, err := r.resolveRemoteID(ctx, ib.Tag)
	if err != nil {
		// Can't confirm the delete reached the node — surface it so the caller
		// marks the node dirty and a reconcile converges, instead of silently
		// dropping the delete and letting the next snapshot resurrect the client.
		return fmt.Errorf("remote DeleteUser: resolve tag %q: %w", ib.Tag, err)
	}
	body := map[string]any{"inboundIds": []int{id}}
	_, err = r.do(ctx, http.MethodPost,
		"panel/api/clients/"+url.PathEscape(email)+"/detach", body)
	if err == nil {
		return nil
	}
	var apiErr *remoteAPIError
	if errors.As(err, &apiErr) && strings.Contains(strings.ToLower(apiErr.msg), "not found") {
		return nil
	}
	return err
}

func (r *Remote) DeleteClient(ctx context.Context, email string) error {
	if email == "" {
		return nil
	}

View on GitHub (pinned to ad32144c42)

Solutions

  1. If the node was merely offline: bring it back and let the dirty-node reconcile (or a retry of the delete) complete the detach.
  2. If the inbound genuinely no longer exists on the node, the delete is already effectively done — remove the stale central record/inbound and let reconcile clean up.
  3. Do not wrap this call in a silent success-on-error; the error is what drives convergence.
  4. Check node health (heartbeat status) before bulk client deletions on node-assigned inbounds.

Example fix

// before: caller ignores the error, client resurrects after next sync
_ = remote.DeleteUser(ctx, ib, email)

// after: propagate so the node is marked dirty and reconcile finishes the delete
if err := remote.DeleteUser(ctx, ib, email); err != nil {
    return err // caller marks node dirty; ReconcileNode converges
}
Defensive patterns

Strategy: retry

Validate before calling

if node, err := nodeStatus(ctx, nodeID); err != nil || !node.Online {
    return fmt.Errorf("node offline; delete will be retried by reconcile")
}

Type guard

func isDeleteResolveFailure(err error) bool {
    return err != nil && strings.HasPrefix(err.Error(), "remote DeleteUser: resolve tag")
}

Try / catch

if err := remote.DeleteUser(ctx, ib, email); err != nil {
    // propagate — the error is the signal that marks the node dirty;
    // ReconcileNode finishes the delete on convergence
    markDirtyAndReconcile(nodeID)
    return err
}

Prevention

When it happens

Trigger: Deleting a client from a node-assigned inbound while the node is unreachable, or the inbound tag no longer resolves on the node. The detach RPC is skipped because the node-local inbound ID is unknown.

Common situations: Client deleted on the master while the node is temporarily down; inbound already removed on the node so the central delete cannot address it.

Related errors


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