{"record":{"id":"2fadce0aa748908d","repo":"XTLS/Xray-core","slug":"user-email-already-exists","errorCode":null,"errorMessage":"User ${email} already exists.","messagePattern":"User (.+?) already exists\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/trojan/validator.go","lineNumber":23,"sourceCode":"\t\"sync\"\n\n\t\"github.com/xtls/xray-core/common/errors\"\n\t\"github.com/xtls/xray-core/common/protocol\"\n)\n\n// Validator stores valid trojan users.\ntype Validator struct {\n\t// Considering email's usage here, map + sync.Mutex/RWMutex may have better performance.\n\temail sync.Map\n\tusers sync.Map\n}\n\n// Add a trojan user, Email must be empty or unique.\nfunc (v *Validator) Add(u *protocol.MemoryUser) error {\n\tif u.Email != \"\" {\n\t\t_, loaded := v.email.LoadOrStore(strings.ToLower(u.Email), u)\n\t\tif loaded {\n\t\t\treturn errors.New(\"User \", u.Email, \" already exists.\")\n\t\t}\n\t}\n\tv.users.Store(hexString(u.Account.(*MemoryAccount).Key), u)\n\treturn nil\n}\n\n// Del a trojan user with a non-empty Email.\nfunc (v *Validator) Del(e string) error {\n\tif e == \"\" {\n\t\treturn errors.New(\"Email must not be empty.\")\n\t}\n\tle := strings.ToLower(e)\n\tu, _ := v.email.Load(le)\n\tif u == nil {\n\t\treturn errors.New(\"User \", e, \" not found.\")\n\t}\n\tv.email.Delete(le)\n\tv.users.Delete(hexString(u.(*protocol.MemoryUser).Account.(*MemoryAccount).Key))","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/XTLS/Xray-core/blob/7d214f8b094f75322fa3990f8aadad1c912f24f5/proxy/trojan/validator.go#L5-L41","documentation":"Validator.Add rejected a trojan user because another user with the same email (compared lowercased) is already registered in this inbound. Emails are the user-facing key for management APIs (stats, del), so uniqueness (case-insensitive) is enforced at add time.","triggerScenarios":"Calling the trojan inbound's Add with an email that exists — typically via the API handler (proxy.AddInboundHandler / handler.service) or config reload that merges duplicate client entries.","commonSituations":"Copy-pasted 'clients' entries with the same email in config; API-driven user provisioning retrying after a partial success; email differing only by case ('Alice' vs 'alice').","solutions":["Remove or rename the duplicate email in the inbound's clients list before reloading","If driving via API, list existing users first and skip/replace instead of re-adding","Remember comparison is case-insensitive: 'A@x' collides with 'a@x'"],"exampleFix":"// before\n\"clients\": [\n  {\"password\": \"p1\", \"email\": \"user@x\"},\n  {\"password\": \"p2\", \"email\": \"user@x\"}\n]\n// after\n\"clients\": [\n  {\"password\": \"p1\", \"email\": \"user@x\"},\n  {\"password\": \"p2\", \"email\": \"user2@x\"}\n]","handlingStrategy":"validation","validationCode":"// before adding, check for the (case-insensitive) email collision\nfunc canAdd(v *Validator, email string) bool {\n    if email == \"\" { return true }\n    _, exists := v.email.Load(strings.ToLower(email))\n    return !exists\n}","typeGuard":null,"tryCatchPattern":"if err := validator.Add(user); err != nil {\n    if strings.Contains(err.Error(), \"already exists\") {\n        // idempotent provisioning: treat as success or update-in-place\n        return nil\n    }\n    return err\n}","preventionTips":["Deduplicate the clients array in config before reload (CI lint or pre-flight script)","Make API-driven provisioning idempotent: check-then-add keyed on lowercased email","Never reuse one email across users; emails drive stats and deletion lookups"],"tags":["trojan","users","config"],"backgroundTag":null,"analyzedSha":"7d214f8b094f75322fa3990f8aadad1c912f24f5","analyzedAt":"2026-08-15T14:26:24.325Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}