{"record":{"id":"9cb52b9471746d73","repo":"Tencent/WeKnora","slug":"oauth-attempt-not-found-or-expired","errorCode":null,"errorMessage":"oauth attempt not found or expired","messagePattern":"oauth attempt not found or expired","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/mcp/oauth_state.go","lineNumber":131,"sourceCode":"\t\t_, err = pipe.Exec(ctx)\n\t\treturn err\n\t}\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\texpiresAt := time.Now().Add(oauthStateTTL)\n\ts.mem[state] = memStateEntry{value: value, expiresAt: expiresAt}\n\ts.attempts[state] = memAttemptEntry{value: attempt, expiresAt: expiresAt}\n\treturn nil\n}\n\n// CompleteAttempt marks an authorization attempt complete only after the code\n// exchange has successfully persisted a token.\nfunc (s *oauthStateStore) CompleteAttempt(ctx context.Context, state string) error {\n\tif s.rdb != nil {\n\t\tdata, err := s.rdb.Get(ctx, s.attemptKey(state)).Bytes()\n\t\tif err != nil {\n\t\t\tif err == redis.Nil {\n\t\t\t\treturn fmt.Errorf(\"oauth attempt not found or expired\")\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tvar attempt OAuthAttempt\n\t\tif err := json.Unmarshal(data, &attempt); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tattempt.Completed = true\n\t\tdata, err = json.Marshal(attempt)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn s.rdb.Set(ctx, s.attemptKey(state), data, oauthStateTTL).Err()\n\t}\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\tentry, ok := s.attempts[state]\n\tif !ok || time.Now().After(entry.expiresAt) {","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/mcp/oauth_state.go#L113-L149","documentation":"CompleteAttempt marks an OAuth authorization attempt as completed after the token exchange succeeded. In Redis-backed mode it looks up the attempt key; redis.Nil (missing key) yields this error. The attempt either never existed, already completed and was consumed, or its TTL expired.","triggerScenarios":"CompleteAuthorization calls CompleteAttempt with a state whose Redis key attemptKey(state) no longer exists (expired TTL, eviction, flushed Redis, or state never created via the store).","commonSituations":"User takes longer than the oauthStateTTL to finish login; Redis restarted without persistence; callback delivered twice and the second completion finds the key gone; deployment pointed at a different Redis instance than the one that issued the state.","solutions":["Restart the OAuth flow: generate a new state and send the user through authorization again.","Confirm the callback is arriving within oauthStateTTL; increase TTL if flows are legitimately slow.","Verify the callback handler and CompleteAttempt use the same Redis instance and key namespace.","Make callbacks idempotent: if the attempt is missing but a token was already stored, treat completion as done instead of erroring."],"exampleFix":"// before\nif err := store.CompleteAttempt(ctx, state); err != nil { return err }\n// after\nif err := store.CompleteAttempt(ctx, state); err != nil {\n    if _, tokErr := store.Attempt(ctx, state); tokErr != nil {\n        return redirectUserToRestartOAuthFlow() // stale/expired state\n    }\n    return err\n}","handlingStrategy":"retry","validationCode":"if err := rdb.Exists(ctx, \"mcp:oauth:attempt:\"+state).Err(); err != nil || rdb.Exists(ctx, \"mcp:oauth:attempt:\"+state).Val() == 0 {\n    // state gone; restart flow instead of completing\n}","typeGuard":null,"tryCatchPattern":"if err := store.CompleteAttempt(ctx, state); err != nil {\n    if strings.Contains(err.Error(), \"not found or expired\") {\n        return restartAuthorization(ctx, principal) // fresh state\n    }\n    return err\n}","preventionTips":["Complete the attempt within oauthStateTTL; surface a countdown in the login UI.","Share one Redis instance/namespace across all replicas handling callbacks.","Make the callback idempotent: if a token already exists for the principal, treat replay as success.","Enable Redis persistence (AOF/RDB) so restarts don't wipe in-flight attempts."],"tags":["oauth","state-expired","redis","ttl"],"backgroundTag":"oauth-state-expired","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}