Skip to content

Handle failed logins gracefully without automatic retries - #84

Merged
popen2 merged 1 commit into
mainfrom
claude/issue-83-jk36qb
Aug 21, 2026
Merged

popen2 merged 1 commit into
mainfrom
claude/issue-83-jk36qb

Conversation

@popen2

@popen2 popen2 commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Summary

This PR fixes issue #83 where failed login attempts were retried automatically every 500ms, causing multiple login tabs/windows to accumulate when a machine was left unattended. The fix distinguishes between different failure modes and applies appropriate retry strategies: logins that nobody completed are no longer retried automatically, while other transient failures back off exponentially.

Key Changes

  • Login abort handling: Added cancelledByUser flag to LoginAbortedError to distinguish user-initiated cancellations (window closed, sign-in denied) from other failures. Abandoned logins are no longer retried on a timer.

  • Exponential backoff for transient failures: Implemented exponential backoff for non-login failures (network issues, AWS errors) that double the retry delay with each consecutive failure, capped at 30 minutes. This prevents hammering AWS with repeated requests for permanent configuration issues.

  • Token validity checking: Added hasValidToken() to detect when the token refresh succeeded but a downstream step (profile refresh, EKS scan) failed. These failures now keep the ordinary token expiry schedule instead of triggering error retries.

  • Scheduled refresh tracking: Added getNextRefreshAt() and cancelTokenRefresh() functions to expose the scheduled refresh time to the UI, allowing the tray to show "Sign-in needed" instead of claiming a refresh is due when one has been deliberately cancelled.

  • Improved error handling in token polling:

    • Added handling for SlowDownException with dynamic interval adjustment per RFC 8628
    • Added handling for AccessDeniedException to detect user denial at identity provider
    • Improved error messages with describeError() wrapper
    • Added proper ErrorOptions with cause field to error constructors
  • Notification on abandoned login: Added notifyLoginNeeded() to notify the user once when a login is abandoned (unless they closed the window themselves), keeping the browser clean while making the situation visible.

  • Documentation updates: Updated troubleshooting and login documentation to explain the new behavior and fix for the tab accumulation issue.

Implementation Details

  • The scheduleAfterFailure() function centralizes retry logic with three distinct paths: valid token (use expiry schedule), abandoned login (cancel refresh), or transient failure (exponential backoff).
  • Consecutive failure count is tracked in module state and reset on successful runs or when a valid token is obtained.
  • The tray menu now reads getNextRefreshAt() instead of expiresAt to accurately reflect whether Frost is actively waiting for a refresh or waiting for user action.
  • Added comprehensive test cases in schedule.ts to verify backoff behavior and edge cases.

https://claude.ai/code/session_01MgEBPwgAkov8Q1nFyJ78mt

@popen2
popen2 force-pushed the claude/issue-83-jk36qb branch 3 times, most recently from 1d05a6d to dd75d17 Compare August 21, 2026 08:14
Follow-up to #82, which fixed the 500ms retry loop behind #83. Three things
that fix leaves open, plus the parts of the login path around it.

The one that matters: retryDelayMsAfterError() was applied to every non-abort
failure, including one that happens *after* the token was renewed. Those retries
re-enter getNewToken(), so a failing profiles or EKS step opened a login page
every 60 seconds while the token it already held was valid for another eight
hours — in default-browser mode, a tab a minute. scheduleAfterFailure() now
checks for a valid token first and keeps the ordinary expiry schedule there.

- Frost stopping after an abandoned login was invisible: the tray read
  `expiresAt`, so it announced "Next refresh 8 hours ago" with nothing
  scheduled. It now reads the scheduled time (getNextRefreshAt()) and says
  "Sign-in needed", and a single notification says so too — not when the user
  closed the window or refused the sign-in themselves, which LoginAbortedError
  now carries as `cancelledByUser`.
- Repeated failures back off 1m→30m instead of asking AWS the same question
  every minute forever; the streak resets on a clean run.
- AccessDeniedException ends the run like ExpiredTokenException already does,
  and SlowDownException widens the poll interval (RFC 8628 §3.5) instead of
  being logged and ignored.
- Default `expiresIn`/`interval` when AWS omits them: the first made the poll
  loop exit before it ran, the second made the sleep NaN and the loop hot.
- Describe the openExternal failure with describeError(), per AGENTS.md.
- docs/docs/{credential-refresh,login,troubleshooting}.html describe the new
  behaviour, and AGENTS.md gains schedule.ts with the two rules that keep #83
  fixed.

Also removes the assert-based self-check block from schedule.ts.

Verified with `npm run build`, `npm run lint`, and a headless harness that loads dist/aws-sso.js against stubbed electron and a faked
SSO OIDC client (15 checks). The post-login-failure case is the one that changed
against main: token valid for an hour with every later step failing opens 2
login pages in 70s on main, 1 here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgEBPwgAkov8Q1nFyJ78mt
@popen2
popen2 force-pushed the claude/issue-83-jk36qb branch from dd75d17 to e414ab8 Compare August 21, 2026 08:25
@popen2
popen2 enabled auto-merge August 21, 2026 08:27
@popen2
popen2 merged commit d70419a into main Aug 21, 2026
9 checks passed
@popen2
popen2 deleted the claude/issue-83-jk36qb branch August 21, 2026 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants