Repository navigation
fix(cli): handle refreshAuth failures gracefully in non-interactive mode - #28848
chelsealong wants to merge 2 commits into
Conversation
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.
|
📊 PR Size: size/M
|
Summary of ChangesHello, 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
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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.
| if (config.getOutputFormat() === OutputFormat.JSON) { | ||
| handleError( | ||
| error instanceof Error ? error : new Error(String(error)), | ||
| config, | ||
| ExitCodes.FATAL_AUTHENTICATION_ERROR, | ||
| ); |
There was a problem hiding this comment.
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,
);
}
| 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']; | ||
| } |
There was a problem hiding this comment.
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
- When testing code that depends on environment variables, use
vi.stubEnv('NAME', 'value')inbeforeEachandvi.unstubAllEnvs()inafterEach. Avoid modifyingprocess.envdirectly as it can lead to test leakage and is less reliable. (link)
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.
|
Addressed both review comments:
Verified the target test still passes ( |
|
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. |
|
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. |
Summary
When
refreshAuth()fails during non-interactive (--prompt) startup, theCLI 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()inpackages/cli/src/gemini.tsxcallsconfig.refreshAuth(authType)twice on the non-interactive path:
try/catchthat records
initialAuthFailed— but that flag is only ever checkedinside the
if (sandboxConfig)branch, right before relaunching into asandboxed child process.
Confighas beenloaded — with no error handling at all.
For most CLI users sandboxing is not enabled, so
sandboxConfigisundefined, theinitialAuthFailedcheck is never reached, and the second,unprotected
refreshAuth()call runs the same auth flow again. When itthrows — e.g. because Google's Code Assist backend rejects a legacy
oauth-personalclient withUNSUPPORTED_CLIENT(as reported in #28846) —the rejection is not caught anywhere in
main(), so it propagates to thetop-level
main().catch(...)handler inpackages/cli/index.ts, whichtreats 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 atry/catch, mirroringthe existing error-handling pattern already used in
validateNonInteractiveAuth(JSON output →handleError, text output →log +
process.exit(ExitCodes.FATAL_AUTHENTICATION_ERROR)). Users now get areadable "Failed to authenticate: " error (which, for the
UNSUPPORTED_CLIENTcase, already includes the server-provided migrationguidance) 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.tsxnpx tsc -p packages/cli/tsconfig.json --noEmitTest output
The new test was verified to fail against the pre-fix code (uncaught
Error: This client is no longer supportedinstead of the expectedMockProcessExitErrorwith 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.tsxtests (e.g. "should read from stdin innon-interactive mode", "project hooks loading based on trust > ...") to
fail with
FatalUntrustedWorkspaceErrorwhen the whole file is runtogether. This reproduces identically on unmodified
mainand is notcaused by this change.
Pre-Merge Checklist
oauth-personalaccount to reproduce manually)AI assistance disclosure
This change (analysis, implementation, and tests) was prepared with AI
assistance (Claude Code / Anthropic).