Skip to content

fix(cli): handle refreshAuth failures gracefully in non-interactive mode - #28848

Closed
chelsealong wants to merge 2 commits into
google-gemini:mainfrom
chelsealong:fix-28846-non-interactive-auth-crash
Closed

chelsealong wants to merge 2 commits into
google-gemini:mainfrom
chelsealong:fix-28846-non-interactive-auth-crash

Conversation

@chelsealong

Copy link
Copy Markdown

Summary

When refreshAuth() fails during non-interactive (--prompt) startup, the
CLI can crash with an uncaught raw stack trace and exit code 1 instead of a
clean, actionable error and the dedicated auth error exit code.

Details

main() in packages/cli/src/gemini.tsx calls config.refreshAuth(authType)
twice on the non-interactive path:

  1. Once early (before the sandbox-launch decision), wrapped in a try/catch
    that records initialAuthFailed — but that flag is only ever checked
    inside the if (sandboxConfig) branch, right before relaunching into a
    sandboxed child process.
  2. Once again later, unconditionally, after the full Config has been
    loaded — with no error handling at all.

For most CLI users sandboxing is not enabled, so sandboxConfig is
undefined, the initialAuthFailed check is never reached, and the second,
unprotected refreshAuth() call runs the same auth flow again. When it
throws — e.g. because Google's Code Assist backend rejects a legacy
oauth-personal client with UNSUPPORTED_CLIENT (as reported in #28846) —
the rejection is not caught anywhere in main(), so it propagates to the
top-level main().catch(...) handler in packages/cli/index.ts, which
treats it as "An unexpected critical error occurred" and dumps a raw stack
trace, exiting with code 1 instead of ExitCodes.FATAL_AUTHENTICATION_ERROR.

This PR wraps that second refreshAuth() call in a try/catch, mirroring
the existing error-handling pattern already used in
validateNonInteractiveAuth (JSON output → handleError, text output →
log + process.exit(ExitCodes.FATAL_AUTHENTICATION_ERROR)). Users now get a
readable "Failed to authenticate: " error (which, for the
UNSUPPORTED_CLIENT case, already includes the server-provided migration
guidance) and exit code 41 instead of an unhandled crash.

This does not change the underlying auth/network behavior — the account
still needs to migrate or re-authenticate — it only ensures the failure is
reported cleanly instead of crashing with an uncaught exception.

Related Issues

Related to #28846

How to Validate

  • npx vitest run src/gemini.test.tsx -t "should exit with 41 instead of crashing" (new test)
  • npx eslint packages/cli/src/gemini.tsx packages/cli/src/gemini.test.tsx
  • npx tsc -p packages/cli/tsconfig.json --noEmit

Test output

✓ src/gemini.test.tsx (45 tests | 44 skipped) 540ms
  ✓ gemini.tsx main function exit codes > should exit with 41 instead of crashing when refreshAuth fails in non-interactive mode 538ms

 Test Files  1 passed (1)
      Tests  1 passed | 44 skipped (45)

The new test was verified to fail against the pre-fix code (uncaught
Error: This client is no longer supported instead of the expected
MockProcessExitError with code 41), and to pass after the fix.

Note: this repo checkout's sandbox environment is not a "trusted" folder
per GEMINI_CLI_TRUST_WORKSPACE, which causes several pre-existing,
unrelated gemini.test.tsx tests (e.g. "should read from stdin in
non-interactive mode", "project hooks loading based on trust > ...") to
fail with FatalUntrustedWorkspaceError when the whole file is run
together. This reproduces identically on unmodified main and is not
caused by this change.

Pre-Merge Checklist

  • Added/updated tests
  • Updated relevant documentation and README (not needed — internal error-handling fix)
  • Noted breaking changes (none)
  • Validated on required platforms/methods (validated via unit tests only; no access to a live deprecated oauth-personal account to reproduce manually)

AI assistance disclosure

This change (analysis, implementation, and tests) was prepared with AI
assistance (Claude Code / Anthropic).

The second refreshAuth() call in main() (used when headless auth differs
from the pre-sandbox check, e.g. no sandbox is configured) was not
wrapped in a try/catch. Any auth failure there — such as an
oauth-personal account rejected with UNSUPPORTED_CLIENT — propagated as
an uncaught rejection and printed a raw stack trace instead of a clean,
actionable error, then exited with code 1 instead of the dedicated auth
error code.
@chelsealong
chelsealong requested a review from a team as a code owner August 17, 2026 00:08
@github-actions github-actions Bot added the size/m A medium sized PR label Aug 17, 2026
@github-actions

github-actions Bot commented Aug 17, 2026 •

Copy link
Copy Markdown

📊 PR Size: size/M

  • Lines changed: 67
  • Additions: +66
  • Deletions: -1
  • Files changed: 2

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses an issue where the CLI would crash with an unhandled exception when authentication failed during non-interactive execution. By implementing proper error handling for the secondary authentication check, the CLI now provides actionable feedback and a consistent exit code, improving the user experience for automated or scripted environments.

Highlights

  • Error Handling Improvement: Wrapped the secondary refreshAuth() call in main() with a try/catch block to prevent uncaught exceptions during non-interactive CLI startup.
  • Graceful Failure: Ensured that authentication failures now exit with the appropriate FATAL_AUTHENTICATION_ERROR code (41) and a readable error message instead of crashing with a raw stack trace.
  • Test Coverage: Added a new unit test in gemini.test.tsx to verify that the CLI exits with code 41 when refreshAuth fails in non-interactive mode.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces error handling for authentication failures during refreshAuth in non-interactive mode, ensuring the CLI exits gracefully with a fatal authentication error code instead of crashing, and adds a corresponding unit test. The review feedback recommends extending the JSON error handling to cover the STREAM_JSON output format to avoid protocol violations, and refactoring the test to use vi.stubEnv instead of directly modifying process.env to align with repository testing conventions.

Comment thread packages/cli/src/gemini.tsx Outdated
Comment on lines +875 to +880
if (config.getOutputFormat() === OutputFormat.JSON) {
handleError(
error instanceof Error ? error : new Error(String(error)),
config,
ExitCodes.FATAL_AUTHENTICATION_ERROR,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

When refreshAuth fails in non-interactive mode, the CLI should handle the failure gracefully for all programmatic output formats, including STREAM_JSON. Currently, the check only covers OutputFormat.JSON, which means if the output format is STREAM_JSON, it will fall back to printing a raw text error to stderr and exiting directly. This violates the JSON stream protocol, as downstream consumers expect a structured RESULT event with an error status.

We should update the condition to check for both OutputFormat.JSON and OutputFormat.STREAM_JSON so that handleError is called for both formats.

      if (
        config.getOutputFormat() === OutputFormat.JSON ||
        config.getOutputFormat() === OutputFormat.STREAM_JSON
      ) {
        handleError(
          error instanceof Error ? error : new Error(String(error)),
          config,
          ExitCodes.FATAL_AUTHENTICATION_ERROR,
        );
      }

Comment thread packages/cli/src/gemini.test.tsx Outdated
Comment on lines +1513 to +1522
process.env['GEMINI_API_KEY'] = 'test-key';
try {
await main();
expect.fail('Should have thrown MockProcessExitError');
} catch (e) {
expect(e).toBeInstanceOf(MockProcessExitError);
expect((e as MockProcessExitError).code).toBe(41);
} finally {
delete process.env['GEMINI_API_KEY'];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

According to the repository's testing conventions, direct modification of process.env should be avoided to prevent test leakage and ensure reliability. Instead, use vi.stubEnv('NAME', 'value') to set environment variables. Since afterEach already calls vi.unstubAllEnvs(), we can safely remove the finally block and the manual deletion of the environment variable.

    vi.stubEnv('GEMINI_API_KEY', 'test-key');
    try {
      await main();
      expect.fail('Should have thrown MockProcessExitError');
    } catch (e) {
      expect(e).toBeInstanceOf(MockProcessExitError);
      expect((e as MockProcessExitError).code).toBe(41);
    }
References
  1. When testing code that depends on environment variables, use vi.stubEnv('NAME', 'value') in beforeEach and vi.unstubAllEnvs() in afterEach. Avoid modifying process.env directly as it can lead to test leakage and is less reliable. (link)

@gemini-cli gemini-cli Bot added priority/p2 Important but can be addressed in a future release. area/security Issues related to security labels Aug 17, 2026
Address gemini-code-assist review: extend the refreshAuth error
handling to cover OutputFormat.STREAM_JSON (handleError already
supports it), and use vi.stubEnv instead of mutating process.env
directly in the new test, per repo testing conventions.
@chelsealong

Copy link
Copy Markdown
Author

Addressed both review comments:

  • packages/cli/src/gemini.tsx: widened the refreshAuth error-handling check to cover OutputFormat.STREAM_JSON in addition to OutputFormat.JSON, so handleError (which already emits a proper RESULT stream event for STREAM_JSON) is used for both programmatic formats.
  • packages/cli/src/gemini.test.tsx: replaced direct process.env['GEMINI_API_KEY'] mutation with vi.stubEnv, removing the manual finally cleanup (the global afterEach in test-setup.ts already calls vi.unstubAllEnvs()).

Verified the target test still passes (npx vitest run src/gemini.test.tsx -t "should exit with 41 instead of crashing"), and eslint/tsc --noEmit are clean.

@gemini-cli

gemini-cli Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Hi there! Thank you for your interest in contributing to Gemini CLI.

To ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'.

This PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

@gemini-cli

gemini-cli Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

This pull request is being closed as it has been open for 14 days without a 'help wanted' designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

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

Labels

area/security Issues related to security priority/p2 Important but can be addressed in a future release. size/m A medium sized PR status/pr-nudge-sent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant