-
Notifications
You must be signed in to change notification settings - Fork 470
fix(gui): dedupe dashboard API token prompts on concurrent 401s #651
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dc6d557
2bee4af
92e9677
96726a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,12 @@ | ||
| let installed = false; | ||
| /** Shared 401 refresh gate — concurrent waiters join one prompt / token resolution. */ | ||
| let promptInFlight: Promise<string | null> | null = null; | ||
| /** | ||
| * After the user cancels (or submits blank) once, suppress further prompts for this page | ||
| * lifetime so a staggered 401 fan-out does not reopen the dialog N times (#647 / Codex). | ||
| * A full reload clears module state and allows prompting again. | ||
| */ | ||
| let promptCancelled = false; | ||
|
|
||
| function needsApiAuth(input: RequestInfo | URL): boolean { | ||
| try { | ||
|
|
@@ -31,6 +38,11 @@ function clearToken(): void { | |
| memoryToken = null; | ||
| } | ||
|
|
||
| /** Clear memory only when it still holds `expected` (avoid wiping a newer concurrent store). */ | ||
| function clearTokenIfCurrent(expected: string | null): void { | ||
| if (expected != null && readToken() === expected) clearToken(); | ||
| } | ||
|
|
||
| function clearLegacySessionToken(): void { | ||
| try { | ||
| sessionStorage.removeItem(LEGACY_TOKEN_KEY); | ||
|
|
@@ -46,11 +58,31 @@ function withToken(input: RequestInfo | URL, init: RequestInit | undefined, toke | |
| return [input, { ...init, headers }]; | ||
| } | ||
|
|
||
| async function promptForToken(): Promise<string | null> { | ||
| /** | ||
| * Resolve a token after a 401. Concurrent callers share one in-flight resolution so a dashboard | ||
| * fan-out does not open one window.prompt per /api request (#647). Re-reads memoryToken before | ||
| * prompting so waiters that wake after another request already stored a token do not re-prompt. | ||
| */ | ||
| async function resolveTokenAfter401(failedToken: string | null): Promise<string | null> { | ||
| if (promptCancelled) return null; | ||
| if (promptInFlight) return promptInFlight; | ||
| promptInFlight = Promise.resolve() | ||
| .then(() => window.prompt("OpenCodex API token")?.trim() || null) | ||
| .finally(() => { promptInFlight = null; }); | ||
|
|
||
| promptInFlight = (async () => { | ||
| if (promptCancelled) return null; | ||
| const current = readToken(); | ||
| if (current && current !== failedToken) return current; | ||
|
|
||
| const prompted = window.prompt("OpenCodex API token")?.trim() || null; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| if (prompted) { | ||
| storeToken(prompted); | ||
| return prompted; | ||
| } | ||
| promptCancelled = true; | ||
| return null; | ||
| })().finally(() => { | ||
| promptInFlight = null; | ||
| }); | ||
|
Comment on lines
+82
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the dashboard starts without a token and the user cancels or submits whitespace, Useful? React with 👍 / 👎.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in 92e9677: |
||
|
|
||
| return promptInFlight; | ||
| } | ||
|
|
||
|
|
@@ -68,14 +100,23 @@ export function installApiAuthFetch(): void { | |
| const response = await originalFetch(firstInput, firstInit); | ||
| if (response.status !== 401) return response; | ||
|
|
||
| if (token) clearToken(); | ||
| const nextToken = await promptForToken(); | ||
| // Another request may have stored a token while this one was in flight (or while prompt blocked). | ||
| const refreshed = readToken(); | ||
| if (refreshed && refreshed !== token) { | ||
|
Comment on lines
+104
to
+105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When concurrent requests were sent with an already-cached stale token, the first 401 can clear it, prompt once, and store the replacement before a later stale 401 resumes. That later callback executes Useful? React with 👍 / 👎.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in 2bee4af / tip: |
||
| const [retryInput, retryInit] = withToken(input, init, refreshed); | ||
| const retry = await originalFetch(retryInput, retryInit); | ||
| if (retry.status !== 401) return retry; | ||
| clearTokenIfCurrent(refreshed); | ||
| } else { | ||
| clearTokenIfCurrent(token); | ||
| } | ||
|
|
||
| const nextToken = await resolveTokenAfter401(token); | ||
| if (!nextToken) return response; | ||
|
|
||
| storeToken(nextToken); | ||
| const [retryInput, retryInit] = withToken(input, init, nextToken); | ||
| const retry = await originalFetch(retryInput, retryInit); | ||
| if (retry.status === 401) clearToken(); | ||
| if (retry.status === 401) clearTokenIfCurrent(nextToken); | ||
| return retry; | ||
| }; | ||
| } | ||
|
|
@@ -85,4 +126,5 @@ export function resetApiAuthFetchForTests(): void { | |
| installed = false; | ||
| memoryToken = null; | ||
| promptInFlight = null; | ||
| promptCancelled = false; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.