Skip to content

fix(gui): bound the 401 session re-bootstrap and make token resolution abort-aware - #1855

Merged
lidge-jun merged 4 commits into
devfrom
codex/gui-auth-unwedge
Aug 16, 2026
Merged

fix(gui): bound the 401 session re-bootstrap and make token resolution abort-aware#1855
lidge-jun merged 4 commits into
devfrom
codex/gui-auth-unwedge

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

Layer 2 of the loading-resilience stack. Layer 1 bounded each resource fetch; this one removes the app-wide chokepoint that could pin every management request at once.

On any 401 the fetch wrapper joins a single shared token resolution, and that resolution awaited rawFetch("/opencodex-session") with no timeout and no caller signal. One stalled bootstrap therefore froze every /api/* request for the lifetime of the page — caller aborts did nothing, and the shared promise only cleared in a finally that never ran. Loopback sessions expire every five minutes, so the exposure re-armed periodically, which is exactly the "refresh fixes it for a while, then it sticks again" symptom.

Measured on a sandboxed instance with the bootstrap stalled and the server otherwise healthy: /api/* traffic dropped to zero for 40s+ while /healthz kept polling, and cold tabs showed permanent skeletons.

Three changes:

  • The bootstrap is bounded (10s) and returns a tri-state result. Only a definitive refusal — 4xx, or an OK document without valid session meta, the non-loopback shape — reaches the admin-token prompt. Anything transient (timeout, abort, network error, 5xx from an intermediate proxy) fails the wave and re-arms on the next 401, so a brief proxy hiccup can no longer pop a credential dialog on a dashboard that needs no token.
  • Callers race the shared resolution against their own abort signal without cancelling it for the others, and the race listener is always removed.
  • A watchdog bounds the bootstrap call for the case a fetch never honors the abort at all. It deliberately does not cover the prompt: that wait is user-controlled, and later waves joining the same pending body is what keeps exactly one dialog on screen.

Design and evidence: devlog/_plan/260816_gui_loading_performance/020_phase2_auth_unwedge.md

Stack (merge bottom-up): #1854 (resource deadline) ← this PR ← hidden pause ← poll consolidation.

Depends on #1854. Review this PR's diff only.

Verification

  • cd gui && bun test tests → 922 pass / 0 fail
  • cd gui && bun run lint, bun run lint:i18n, bun run build → green
  • New gui/tests/api-auth-deadline.test.ts (6 cases): hung bootstrap fails the wave and a later wave re-bootstraps to success; timeout and 5xx never open the prompt while a 4xx refusal does; a caller abort unwinds only that caller with balanced listener add/remove; no page-lifetime poisoning; the retry carries the caller signal; and a signal-dropping zombie bootstrap is bounded while the prompt is never watchdog-bounded.
  • Live browser check with CDP fault injection: during a 401 storm with the bootstrap stalled, waves settle and re-arm; once the injection clears, every tab recovers automatically with no reload.

Audit note: the first review round failed this layer — the watchdog originally raced the whole resolution body, which would have stacked a new admin-token modal every ~15s on non-loopback dashboards. Fixed and re-reviewed before this PR.

No visual change: this is auth/request plumbing.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ffb91263-0659-4280-8ad5-71b78782821e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Aug 16, 2026
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed.

UI screenshot waived by the gui-screenshot-waived label.

Hygiene

Deterministic PR hygiene checks passed.

@github-actions
github-actions Bot marked this pull request as draft August 16, 2026 16:35
@lidge-jun lidge-jun added the gui-screenshot-waived Maintainer waiver for false-positive GUI screenshot requirements label Aug 16, 2026
@github-actions
github-actions Bot marked this pull request as ready for review August 16, 2026 16:36

@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: 91e4e8e064

ℹ️ 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 153 to 157
const stored = storeSession(
metaContentFromHtml(html, "opencodex-session-token"),
metaContentFromHtml(html, "opencodex-session-csrf"),
metaContentFromHtml(html, "opencodex-session-origin"),
);

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 Prevent abandoned bootstraps from storing late sessions

When the watchdog wins because a bootstrap fetch ignores its abort signal, reBootstrapSessionToken() continues running outside the race. If a later resolution stores a valid session and the abandoned response subsequently settles—for example, after a proxy restart or delayed fault-injection release—this unconditional storeSession overwrites the newer credentials with the late session, which may already be invalid and can restart the 401 cycle. Return parsed credentials without mutating global state, then store them only after that bootstrap is confirmed as the winning resolution (or guard the mutation with a resolution generation).

Useful? React with 👍 / 👎.

…n abort-aware

The shared resolutionInFlight promise awaited reBootstrapSessionToken with no timeout and no caller signal: one stalled /opencodex-session wedged every /api/* fetch for the page lifetime (loopback sessions expire every 5 minutes, so it re-armed periodically). The bootstrap is now bounded (10s) and returns a tri-state minted/unavailable/failed; only a definitive refusal (4xx or meta-less OK) reaches the admin-token prompt, while transient failures (timeout, abort, network, 5xx) fail the wave and re-arm on the next 401. Callers race the shared resolution against their own abort signal without cancelling it for others, and the race listener is always removed. New tests: api-auth-deadline (4 cases).

Evidence: devlog/_plan/260816_gui_loading_performance/001 (E2)
…artifact correction

Live verification found the residual: a bootstrap fetch that never honors the client abort (debugger-held or quirky network stack) leaves the shared resolution body pending forever even with the 10s fetch bound, re-wedging every /api waiter. A 15s whole-resolution watchdog now unwraps the wave as failed, and the conditional clear lets the next 401 start a fresh resolution (a late zombie settle cannot wipe it). Also corrects 001/E3: the post-heal non-recovery observation was a harness artifact (raw CDP rejects Fetch.disable); with interception properly cleared, all tabs recover automatically without reload.
…rompt

Review round r3 (FAIL, one High) caught it: a whole-body watchdog fires 15s into a user-controlled admin-token prompt on non-loopback dashboards, and since promptForAdminToken has no singleton guard each subsequent poll wave stacked another modal, with cancel poisoning promptCancelled. The watchdog now races only the bootstrap call inside the shared body; while the prompt pends, later waves join the same body (single dialog). New regression test: watchdog never bounds the prompt, waves join, single prompt across a >watchdog window.
@lidge-jun
lidge-jun force-pushed the codex/gui-resource-deadline branch from 1ae1011 to 63cd5d2 Compare August 16, 2026 17:02
@lidge-jun
lidge-jun force-pushed the codex/gui-auth-unwedge branch from 91e4e8e to 2511302 Compare August 16, 2026 17:02
@lidge-jun
lidge-jun changed the base branch from codex/gui-resource-deadline to dev August 16, 2026 17:16
@lidge-jun
lidge-jun merged commit 14c643b into dev Aug 16, 2026
60 of 77 checks passed
@lidge-jun
lidge-jun deleted the codex/gui-auth-unwedge branch August 17, 2026 10:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working gui-screenshot-waived Maintainer waiver for false-positive GUI screenshot requirements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant