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
- If the node was merely offline: bring it back and let the dirty-node reconcile (or a retry of the delete) complete the detach.
- 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.
- Do not wrap this call in a silent success-on-error; the error is what drives convergence.
- 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
- Do not swallow this error in wrappers — resurrection of deleted clients is the direct consequence.
- Verify node reachability before bulk deletes so reconciles stay small.
- Monitor dirty-node counts; a persistent dirty node hides an unresolved delete.
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
- remote inbound with tag %q not found on node %s
- remote AddClient: resolve tag %q: %w
- %s %s: %w
- %s %s: HTTP %d: %q
- %s %s: HTTP %d
AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15).
Data as JSON: /api/errors/a802abd2f38723ee.
Report an issue: GitHub.