{"record":{"id":"a73299aeb7285877","repo":"MHSanaei/3x-ui","slug":"token-not-found","errorCode":null,"errorMessage":"token not found","messagePattern":"token not found","errorType":"http","errorClass":null,"httpStatus":200,"severity":"warning","filePath":"internal/web/service/panel/api_token.go","lineNumber":131,"sourceCode":"func (s *ApiTokenService) Delete(id int) error {\n\tif id <= 0 {\n\t\treturn common.NewError(\"invalid token id\")\n\t}\n\tdb := database.GetDB()\n\treturn db.Where(\"id = ?\", id).Delete(model.ApiToken{}).Error\n}\n\nfunc (s *ApiTokenService) SetEnabled(id int, enabled bool) error {\n\tif id <= 0 {\n\t\treturn common.NewError(\"invalid token id\")\n\t}\n\tdb := database.GetDB()\n\tres := db.Model(model.ApiToken{}).Where(\"id = ?\", id).Update(\"enabled\", enabled)\n\tif res.Error != nil {\n\t\treturn res.Error\n\t}\n\tif res.RowsAffected == 0 {\n\t\treturn errors.New(\"token not found\")\n\t}\n\treturn nil\n}\n\n// Match returns true when the presented bearer token matches any enabled\n// row in api_tokens. Tokens are stored as SHA-256 hashes, so the presented\n// value is hashed before a constant-time compare per row keeps a remote\n// attacker from timing the comparison byte-by-byte.\nfunc (s *ApiTokenService) Match(presented string) bool {\n\tif presented == \"\" {\n\t\treturn false\n\t}\n\tdb := database.GetDB()\n\tvar rows []*model.ApiToken\n\tif err := db.Model(model.ApiToken{}).Where(\"enabled = ?\", true).Find(&rows).Error; err != nil {\n\t\treturn false\n\t}\n\tpresentedHash := []byte(crypto.HashTokenSHA256(presented))","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/MHSanaei/3x-ui/blob/ad32144c42455696ea9f14e12168beac3e25f5d2/internal/web/service/panel/api_token.go#L113-L149","documentation":"ApiTokenService.SetEnabled updates the api_tokens row and distinguishes 'no such token' (RowsAffected == 0) from a real DB error. The id either never existed or was already deleted; note GORM's Update reports 0 affected rows for both a missing id and an update that writes the same value depending on dialect — here the guard runs after a clean Update, so a missing/deleted row is the cause. id<=0 is rejected separately as 'invalid token id'.","triggerScenarios":"Toggling enable/disable on a token row that another admin/tab already deleted; stale UI list after token rotation; passing an id of 0 from an unselected list row (that yields 'invalid token id' instead).","commonSituations":"Two admins managing API tokens concurrently; frontend cache showing removed tokens.","solutions":["Refresh the token list and retry against an id that still exists","If the token was intentionally removed, treat this as success in the UI (idempotent delete-then-toggle)","Confirm the id being sent matches the row in api_tokens"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if id <= 0 {\n    return errors.New(\"invalid token id\")\n}\nexists, _ := tokenService.Exists(id)\nif !exists { return nil } // treat as already deleted","typeGuard":null,"tryCatchPattern":"if err := tokenService.SetEnabled(id, enabled); err != nil {\n    if errors.Is(err, ErrTokenNotFound) || strings.Contains(err.Error(), \"token not found\") {\n        return nil // idempotent: row already gone\n    }\n    return err\n}","preventionTips":["Refresh the token list before toggling after concurrent admin changes","Treat toggle-after-delete as success in UI handlers","Send the id from the freshly loaded row, never a cached one"],"tags":["api-token","validation","concurrency","api"],"backgroundTag":null,"analyzedSha":"ad32144c42455696ea9f14e12168beac3e25f5d2","analyzedAt":"2026-08-15T11:13:23.905Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}