From 0abd2a644e947b7794dd68615d5d3d1553b0b5fd Mon Sep 17 00:00:00 2001 From: Tommaso Sciortino Date: Mon, 23 Jun 2025 18:37:41 -0700 Subject: [PATCH] Improve Auth error messaging (#1358) --- docs/cli/authentication.md | 2 +- packages/cli/src/ui/components/AuthDialog.tsx | 2 +- packages/cli/src/ui/hooks/useAuthCommand.ts | 2 +- packages/cli/src/ui/hooks/useGeminiStream.ts | 4 +- packages/core/src/code_assist/errors.ts | 13 ----- packages/core/src/core/client.ts | 3 +- packages/core/src/core/turn.ts | 7 +-- packages/core/src/index.ts | 1 - packages/core/src/utils/errors.ts | 54 ++++++++++++++++--- 9 files changed, 57 insertions(+), 31 deletions(-) delete mode 100644 packages/core/src/code_assist/errors.ts diff --git a/docs/cli/authentication.md b/docs/cli/authentication.md index f1a8ba58..b74e2b60 100644 --- a/docs/cli/authentication.md +++ b/docs/cli/authentication.md @@ -22,7 +22,7 @@ The Gemini CLI requires you to authenticate with Google's AI services. On initia source ~/.bashrc ``` -3. **Login with Google Work** +3. **Login with Google Workspace** - Use this option to log in with the **Google Workspace Accounts**. This is a paid service for businesses and organizations that provides a suite of productivity tools, including a custom email domain (e.g. your-name@your-company.com), enhanced security features, and administrative controls. These accounts are often managed by an employer or school. - Google Workspace Account must configure a Google Cloud Project Id to use. You can temporarily set the environment variable in your current shell session using the following command: diff --git a/packages/cli/src/ui/components/AuthDialog.tsx b/packages/cli/src/ui/components/AuthDialog.tsx index c0e5d4f8..ba19df70 100644 --- a/packages/cli/src/ui/components/AuthDialog.tsx +++ b/packages/cli/src/ui/components/AuthDialog.tsx @@ -35,7 +35,7 @@ export function AuthDialog({ }, { label: 'Gemini API Key', value: AuthType.USE_GEMINI }, { - label: 'Login with Google Work', + label: 'Login with Google Workspace', value: AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE, }, { label: 'Vertex AI', value: AuthType.USE_VERTEX_AI }, diff --git a/packages/cli/src/ui/hooks/useAuthCommand.ts b/packages/cli/src/ui/hooks/useAuthCommand.ts index 5cc67a07..fe890706 100644 --- a/packages/cli/src/ui/hooks/useAuthCommand.ts +++ b/packages/cli/src/ui/hooks/useAuthCommand.ts @@ -49,7 +49,7 @@ export const useAuthCommand = ( const errorMessage = settings.merged.selectedAuthType === AuthType.LOGIN_WITH_GOOGLE_PERSONAL - ? `Failed to login. Ensure your Google account is not an enterprise account. + ? `Failed to login. Ensure your Google account is not a Workspace account. Message: ${getErrorMessage(e)}` : `Failed to login. Message: ${getErrorMessage(e)}`; setAuthError(errorMessage); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index e045fdeb..86540b68 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -22,7 +22,7 @@ import { GitService, EditorType, ThoughtSummary, - isAuthError, + UnauthorizedError, UserPromptEvent, } from '@gemini-cli/core'; import { type Part, type PartListUnion } from '@google/genai'; @@ -537,7 +537,7 @@ export const useGeminiStream = ( 'GEMINI_DEBUG: Caught error in useGeminiStream.ts:', JSON.stringify(error), ); - if (isAuthError(error)) { + if (error instanceof UnauthorizedError) { onAuthError(); } else if (!isNodeError(error) || error.name !== 'AbortError') { addItem( diff --git a/packages/core/src/code_assist/errors.ts b/packages/core/src/code_assist/errors.ts deleted file mode 100644 index f870ab5c..00000000 --- a/packages/core/src/code_assist/errors.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { GaxiosError } from 'gaxios'; - -export function isAuthError(error: unknown): boolean { - return ( - error instanceof GaxiosError && error.response?.data?.error?.code === 401 - ); -} diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index e6d59db9..36acb3e8 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -292,8 +292,7 @@ export class GeminiClient { throw error; } try { - const parsedJson = JSON.parse(text); - return parsedJson; + return JSON.parse(text); } catch (parseError) { await reportError( parseError, diff --git a/packages/core/src/core/turn.ts b/packages/core/src/core/turn.ts index 85fffd93..cf5e0620 100644 --- a/packages/core/src/core/turn.ts +++ b/packages/core/src/core/turn.ts @@ -20,7 +20,7 @@ import { getResponseText } from '../utils/generateContentResponseUtilities.js'; import { reportError } from '../utils/errorReporting.js'; import { getErrorMessage } from '../utils/errors.js'; import { GeminiChat } from './geminiChat.js'; -import { isAuthError } from '../code_assist/errors.js'; +import { UnauthorizedError, toFriendlyError } from '../utils/errors.js'; // Define a structure for tools passed to the server export interface ServerTool { @@ -224,8 +224,9 @@ export class Turn { value: { ...this.lastUsageMetadata, apiTimeMs: durationMs }, }; } - } catch (error) { - if (isAuthError(error)) { + } catch (e) { + const error = toFriendlyError(e); + if (error instanceof UnauthorizedError) { throw error; } if (signal.aborted) { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 163a4819..3a123452 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -21,7 +21,6 @@ export * from './core/nonInteractiveToolExecutor.js'; export * from './code_assist/codeAssist.js'; export * from './code_assist/oauth2.js'; -export * from './code_assist/errors.js'; // Export utilities export * from './utils/paths.js'; diff --git a/packages/core/src/utils/errors.ts b/packages/core/src/utils/errors.ts index 32139c1a..4787c439 100644 --- a/packages/core/src/utils/errors.ts +++ b/packages/core/src/utils/errors.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { GaxiosError } from 'gaxios'; + export function isNodeError(error: unknown): error is NodeJS.ErrnoException { return error instanceof Error && 'code' in error; } @@ -11,12 +13,50 @@ export function isNodeError(error: unknown): error is NodeJS.ErrnoException { export function getErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; - } else { - try { - const errorMessage = String(error); - return errorMessage; - } catch { - return 'Failed to get error details'; - } + } + try { + return String(error); + } catch { + return 'Failed to get error details'; } } + +export class ForbiddenError extends Error {} +export class UnauthorizedError extends Error {} +export class BadRequestError extends Error {} + +interface ResponseData { + error?: { + code?: number; + message?: string; + }; +} + +export function toFriendlyError(error: unknown): unknown { + if (error instanceof GaxiosError) { + const data = parseResponseData(error); + if (data.error && data.error.message && data.error.code) { + switch (data.error.code) { + case 400: + return new BadRequestError(data.error.message); + case 401: + return new UnauthorizedError(data.error.message); + case 403: + // It's import to pass the message here since it might + // explain the cause like "the cloud project you're + // using doesn't have code assist enabled". + return new ForbiddenError(data.error.message); + default: + } + } + } + return error; +} + +function parseResponseData(error: GaxiosError): ResponseData { + // Inexplicably, Gaxios sometimes doesn't JSONify the response data. + if (typeof error.response?.data === 'string') { + return JSON.parse(error.response?.data) as ResponseData; + } + return typeof error.response?.data as ResponseData; +}