{"record":{"id":"a802abd2f38723ee","repo":"MHSanaei/3x-ui","slug":"remote-deleteuser-resolve-tag-q-w","errorCode":null,"errorMessage":"remote DeleteUser: resolve tag %q: %w","messagePattern":"remote DeleteUser: resolve tag %q: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/web/runtime/remote.go","lineNumber":566,"sourceCode":"\t\t\"client\":     client,\n\t\t\"inboundIds\": []int{id},\n\t}\n\tif _, err := r.do(ctx, http.MethodPost, \"panel/api/clients/add\", payload); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (r *Remote) DeleteUser(ctx context.Context, ib *model.Inbound, email string) error {\n\tif email == \"\" {\n\t\treturn nil\n\t}\n\tid, err := r.resolveRemoteID(ctx, ib.Tag)\n\tif err != nil {\n\t\t// Can't confirm the delete reached the node — surface it so the caller\n\t\t// marks the node dirty and a reconcile converges, instead of silently\n\t\t// dropping the delete and letting the next snapshot resurrect the client.\n\t\treturn fmt.Errorf(\"remote DeleteUser: resolve tag %q: %w\", ib.Tag, err)\n\t}\n\tbody := map[string]any{\"inboundIds\": []int{id}}\n\t_, err = r.do(ctx, http.MethodPost,\n\t\t\"panel/api/clients/\"+url.PathEscape(email)+\"/detach\", body)\n\tif err == nil {\n\t\treturn nil\n\t}\n\tvar apiErr *remoteAPIError\n\tif errors.As(err, &apiErr) && strings.Contains(strings.ToLower(apiErr.msg), \"not found\") {\n\t\treturn nil\n\t}\n\treturn err\n}\n\nfunc (r *Remote) DeleteClient(ctx context.Context, email string) error {\n\tif email == \"\" {\n\t\treturn nil\n\t}","sourceCodeStart":548,"sourceCodeEnd":584,"githubUrl":"https://github.com/MHSanaei/3x-ui/blob/ad32144c42455696ea9f14e12168beac3e25f5d2/internal/web/runtime/remote.go#L548-L584","documentation":"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.","triggerScenarios":"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.","commonSituations":"Client deleted on the master while the node is temporarily down; inbound already removed on the node so the central delete cannot address it.","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."],"exampleFix":"// before: caller ignores the error, client resurrects after next sync\n_ = remote.DeleteUser(ctx, ib, email)\n\n// after: propagate so the node is marked dirty and reconcile finishes the delete\nif err := remote.DeleteUser(ctx, ib, email); err != nil {\n    return err // caller marks node dirty; ReconcileNode converges\n}","handlingStrategy":"retry","validationCode":"if node, err := nodeStatus(ctx, nodeID); err != nil || !node.Online {\n    return fmt.Errorf(\"node offline; delete will be retried by reconcile\")\n}","typeGuard":"func isDeleteResolveFailure(err error) bool {\n    return err != nil && strings.HasPrefix(err.Error(), \"remote DeleteUser: resolve tag\")\n}","tryCatchPattern":"if err := remote.DeleteUser(ctx, ib, email); err != nil {\n    // propagate — the error is the signal that marks the node dirty;\n    // ReconcileNode finishes the delete on convergence\n    markDirtyAndReconcile(nodeID)\n    return err\n}","preventionTips":["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."],"tags":["node-sync","client-management","reconcile","remote"],"backgroundTag":null,"analyzedSha":"ad32144c42455696ea9f14e12168beac3e25f5d2","analyzedAt":"2026-08-15T11:13:23.905Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}