Skip to content

fix(gui): dedupe dashboard API token prompts on concurrent 401s - #651

Merged
Wibias merged 4 commits into
lidge-jun:devfrom
Wibias:fix/647-dashboard-token-prompt-storm
Jul 29, 2026
Merged

fix(gui): dedupe dashboard API token prompts on concurrent 401s#651
Wibias merged 4 commits into
lidge-jun:devfrom
Wibias:fix/647-dashboard-token-prompt-storm

Conversation

@Wibias

@Wibias Wibias commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes Dashboard prompts for the API token once per in-flight /api request on non-loopback binds #647: on non-loopback binds (OPENCODEX_API_AUTH_TOKEN required), the dashboard no longer opens one window.prompt per in-flight /api/* 401.
  • After a 401, re-read the in-memory token (another request may have stored one) and share a single refresh gate so concurrent waiters join one prompt.
  • Preserves memory-only token storage (no sessionStorage write) and cross-origin token isolation.

Test plan

  • cd gui && bun test tests/api-auth-memory.test.ts — concurrent 401 fan-out expects exactly 1 prompt and all 200s
  • Existing memory-only + cross-origin auth tests still pass
  • cd gui && bun run lint && bun run build
  • CI green on this PR

Summary by CodeRabbit

  • Bug Fixes
    • Improved authentication recovery when multiple requests fail with expired/invalid credentials at the same time.
    • Ensured only one token prompt is shown during concurrent 401 responses.
    • Prevented cancellation from repeatedly reopening the prompt, and avoided clearing a newer refreshed token.
    • Retried once using the newly stored token to improve success consistency.
  • Tests
    • Added concurrency-focused tests covering single-prompt behavior, token-staleness safety, and retry outcomes across many simultaneous 401s.

Re-read the in-memory token after a 401 and share one refresh gate so a
management fan-out prompts once instead of once per in-flight /api request
when OPENCODEX_API_AUTH_TOKEN is required. Fixes lidge-jun#647.
@github-actions github-actions Bot added the bug Something isn't working label Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The API authentication wrapper coordinates concurrent 401 responses through shared in-memory token state, prevents stale requests from clearing newer tokens, suppresses repeated prompts after cancellation, and retries requests once. Three tests cover prompt deduplication, token races, and cancellation fan-out.

Changes

API token refresh coordination

Layer / File(s) Summary
Shared token resolution gate
gui/src/api.ts
Adds shared prompt and cancellation state, re-reads memoryToken, conditionally clears only matching tokens, and resets authentication state for tests.
Conditional retry and concurrency validation
gui/src/api.ts, gui/tests/api-auth-memory.test.ts
The 401 handler reuses refreshed tokens before prompting and retries once; tests verify one prompt, preservation of token-v2, and no repeated prompt after cancellation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ConcurrentRequests
  participant installApiAuthFetch
  participant resolveTokenAfter401
  participant window.prompt
  participant originalFetch

  ConcurrentRequests->>installApiAuthFetch: send API requests
  installApiAuthFetch->>originalFetch: issue requests
  originalFetch-->>installApiAuthFetch: return concurrent 401 responses
  installApiAuthFetch->>resolveTokenAfter401: resolve shared token
  resolveTokenAfter401->>window.prompt: prompt once when needed
  window.prompt-->>resolveTokenAfter401: return token or null
  resolveTokenAfter401-->>installApiAuthFetch: provide shared result
  installApiAuthFetch->>originalFetch: retry requests once
  originalFetch-->>ConcurrentRequests: return final statuses
Loading

Possibly related PRs

Suggested labels: bug

Suggested reviewers: ingwannu, lidge-jun

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: deduplicating dashboard API token prompts during concurrent 401 retries.
Linked Issues check ✅ Passed The PR implements the requested one-prompt shared retry flow for concurrent 401s while preserving in-memory token storage and isolation.
Out of Scope Changes check ✅ Passed The new gate, token-clear helper, cancellation handling, and tests all support the linked auth-prompt fix; no unrelated changes stand out.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

Only clear memory when it still holds the failed token, so a sibling request that already stored a valid token is not erased before the refresh retry.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc6d55700d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread gui/src/api.ts
Comment on lines +89 to +90
const refreshed = readToken();
if (refreshed && refreshed !== token) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve a token refreshed by another request

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 clearToken() immediately before this re-read, deleting the replacement, so refreshed is always null and another prompt opens. Guard token clearing with readToken() === token (including the retry paths) so a late failure cannot erase a newer token.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2bee4af / tip: clearTokenIfCurrent only clears when memory still holds the failed token, so a concurrent refresh is preserved. Covered by the new stale concurrent 401 regression test.

Comment thread gui/src/api.ts
Comment on lines +65 to +67
})().finally(() => {
promptInFlight = null;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remember canceled resolutions for the current 401 fan-out

When the dashboard starts without a token and the user cancels or submits whitespace, window.prompt blocks the browser thread so staggered 401 continuations generally cannot join the gate while it is active. Clearing the gate immediately after that null result makes every remaining request open its own prompt, so dismissing one dialog can still produce the full dashboard fan-out of dialogs. Track the completed auth-attempt generation for requests from the same fan-out while still allowing a later, newly initiated request to prompt again.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 92e9677: promptCancelled suppresses further prompts for the rest of the page lifetime after cancel/blank. Covered by the cancel fan-out regression test.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gui/src/api.ts (1)

86-104: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Unconditional clearToken() calls race with concurrent token refreshes — can clobber a good token another request just stored.

Three sites here (Line 86, Line 94, Line 102) call clearToken() based only on the locally-captured stale token being truthy, without re-checking whether the shared memoryToken still equals that stale value. This is a classic TOCTOU/lost-update: between the moment a slow request captured token and the moment its 401 response arrives, a concurrent request may have already prompted and stored a fresh, valid token. The slow request's unconditional clearToken() then wipes out that good token, forcing yet another prompt — reintroducing exactly the prompt-storm behavior this PR (#647) is meant to fix, just under a narrower race window (existing token invalidation with concurrent in-flight requests, rather than the initial no-token bootstrap case the new test covers).

Concretely: request A gets a 401 on stale token T1, clears it, prompts, and stores T2. If request B (also carrying stale T1) resumes its own 401 handling after A stored T2, B's if (token) clearToken() at Line 86 wipes T2 back to null, and B goes on to prompt again since readToken() now returns null.

Guard each clear against the current in-memory value:

🔒 Proposed fix — only clear if memoryToken still matches the token that just failed
-    if (token) clearToken();
+    if (token && readToken() === token) clearToken();

     // Another request may have stored a token while this one was in flight (or while prompt blocked).
     const refreshed = readToken();
     if (refreshed && refreshed !== token) {
       const [retryInput, retryInit] = withToken(input, init, refreshed);
       const retry = await originalFetch(retryInput, retryInit);
       if (retry.status !== 401) return retry;
-      clearToken();
+      if (readToken() === refreshed) clearToken();
     }

     const nextToken = await resolveTokenAfter401(token);
     if (!nextToken) return response;

     const [retryInput, retryInit] = withToken(input, init, nextToken);
     const retry = await originalFetch(retryInput, retryInit);
-    if (retry.status === 401) clearToken();
+    if (retry.status === 401 && readToken() === nextToken) clearToken();
     return retry;

Worth adding a regression test for this scenario (existing token invalidated while a second concurrent request is still holding the stale value) since the current concurrency test only exercises the no-token bootstrap path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/api.ts` around lines 86 - 104, Guard every clearToken call in the 401
retry flow around resolveTokenAfter401 so it only clears when the shared current
token still equals the locally captured failed token. Apply this check to the
initial clear, the refreshed-token retry failure, and the final retry failure,
preserving the existing retry behavior while preventing a concurrent refresh
from being erased.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gui/src/api.ts`:
- Line 62: Update the API token prompt in the authentication flow to use the
existing i18n translation key for the provider alias prompt instead of the
hardcoded “OpenCodex API token” text. Thread the translated string into the code
around the prompted token value, following the established t(...) usage in
CodexAccountPool and useProviderAccountPools.

---

Outside diff comments:
In `@gui/src/api.ts`:
- Around line 86-104: Guard every clearToken call in the 401 retry flow around
resolveTokenAfter401 so it only clears when the shared current token still
equals the locally captured failed token. Apply this check to the initial clear,
the refreshed-token retry failure, and the final retry failure, preserving the
existing retry behavior while preventing a concurrent refresh from being erased.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e3f39e2c-2bed-4406-a340-88dd7856cb0d

📥 Commits

Reviewing files that changed from the base of the PR and between ca7b104 and dc6d557.

📒 Files selected for processing (2)
  • gui/src/api.ts
  • gui/tests/api-auth-memory.test.ts

Comment thread gui/src/api.ts
If the user dismisses the dashboard token dialog, remember the cancel for this page load so staggered 401 continuations do not reopen the prompt for every remaining /api request.
@Wibias

Wibias commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@Wibias

Wibias commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Wibias

Wibias commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 51 seconds.

@Wibias

Wibias commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gui/src/api.ts`:
- Around line 4-9: Confirm that the permanent page-lifetime behavior of
promptCancelled is intentional UX: after cancellation or blank submission,
subsequent /api/* 401 responses must not reopen the prompt until a full reload.
If this is not desired, revise the cancellation state and related prompt flow to
provide an in-app retry path while preserving fan-out suppression.

In `@gui/tests/api-auth-memory.test.ts`:
- Around line 180-236: Extract the duplicated gate polling and release/settle
logic from the affected concurrency tests into shared helpers, such as
waitForGateCount and releaseAndSettleAll. Reuse those helpers in the current
test and the preceding related test, preserving the existing polling limits,
release order, settlement assertions, and test behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5effb695-ebb1-4cf1-a786-02e225ac4d5d

📥 Commits

Reviewing files that changed from the base of the PR and between 2bee4af and 96726a1.

📒 Files selected for processing (2)
  • gui/src/api.ts
  • gui/tests/api-auth-memory.test.ts

Comment thread gui/src/api.ts
Comment thread gui/tests/api-auth-memory.test.ts

@Wibias Wibias left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainer review — merge-ready (cannot self-APPROVE; not merging)

Issue: #647 — dashboard opened one window.prompt per concurrent /api/* 401 under OPENCODEX_API_AUTH_TOKEN. Not fixed on main or prior dev tip; no screenshots on the issue.

Fix (tip 96726a18): shared resolveTokenAfter401 gate, re-read memoryToken, clearTokenIfCurrent, promptCancelled after cancel/blank.

Bot findings addressed:

  • Codex P2 clear-token race → fixed + regression test
  • Codex P2 cancel fan-out → fixed + regression test
  • CodeRabbit critical clearToken → fixed; tip re-review only trivial UX/DRY nits (acknowledged / skipped)
  • CodeRabbit i18n on pre-existing prompt string → out of scope; Rabbit withdrew
  • Codex tip re-review blocked by usage limits; earlier findings verified fixed on tip

Subagent reviews: security — no medium+; defect review — no actionable defects.

CI: all checks green on tip (ubuntu/macos/windows + npm-global + react-doctor + label + CodeRabbit + enforce-target).

Leave unmerged per request.

@Wibias

Wibias commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

Ready to merge.

@Wibias
Wibias merged commit a4fb284 into lidge-jun:dev Jul 29, 2026
11 checks passed
Wibias added a commit that referenced this pull request Jul 29, 2026
@Wibias

Wibias commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Transition complete:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard prompts for the API token once per in-flight /api request on non-loopback binds

1 participant