mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-02 01:11:24 -07:00
feat: better error messages (#20577)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com> # Conflicts: # packages/cli/src/test-utils/render.tsx # packages/cli/src/ui/AppContainer.tsx # packages/cli/src/ui/contexts/UIActionsContext.tsx # packages/core/src/utils/errors.test.ts # packages/core/src/utils/errors.ts
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
toFriendlyError,
|
||||
BadRequestError,
|
||||
ForbiddenError,
|
||||
AccountSuspendedError,
|
||||
getErrorMessage,
|
||||
} from './errors.js';
|
||||
|
||||
@@ -119,9 +120,86 @@ describe('toFriendlyError', () => {
|
||||
};
|
||||
const result = toFriendlyError(error);
|
||||
expect(result).toBeInstanceOf(ForbiddenError);
|
||||
expect(result).not.toBeInstanceOf(AccountSuspendedError);
|
||||
expect((result as ForbiddenError).message).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('should return AccountSuspendedError for 403 with TOS_VIOLATION reason in details', () => {
|
||||
const error = {
|
||||
response: {
|
||||
data: {
|
||||
error: {
|
||||
code: 403,
|
||||
message:
|
||||
'This service has been disabled in this account for violation of Terms of Service.',
|
||||
details: [
|
||||
{
|
||||
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
|
||||
reason: 'TOS_VIOLATION',
|
||||
domain: 'example.googleapis.com',
|
||||
metadata: {
|
||||
uiMessage: 'true',
|
||||
appeal_url_link_text: 'Appeal Here',
|
||||
appeal_url: 'https://example.com/appeal',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = toFriendlyError(error);
|
||||
expect(result).toBeInstanceOf(AccountSuspendedError);
|
||||
expect(result).toBeInstanceOf(ForbiddenError);
|
||||
const suspended = result as AccountSuspendedError;
|
||||
expect(suspended.message).toBe(
|
||||
'This service has been disabled in this account for violation of Terms of Service.',
|
||||
);
|
||||
expect(suspended.appealUrl).toBe('https://example.com/appeal');
|
||||
expect(suspended.appealLinkText).toBe('Appeal Here');
|
||||
});
|
||||
|
||||
it('should return ForbiddenError for 403 with violation message but no TOS_VIOLATION detail', () => {
|
||||
const error = {
|
||||
response: {
|
||||
data: {
|
||||
error: {
|
||||
code: 403,
|
||||
message:
|
||||
'This service has been disabled in this account for violation of Terms of Service.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = toFriendlyError(error);
|
||||
expect(result).toBeInstanceOf(ForbiddenError);
|
||||
expect(result).not.toBeInstanceOf(AccountSuspendedError);
|
||||
});
|
||||
|
||||
it('should return ForbiddenError for 403 with non-TOS_VIOLATION detail', () => {
|
||||
const error = {
|
||||
response: {
|
||||
data: {
|
||||
error: {
|
||||
code: 403,
|
||||
message: 'Forbidden',
|
||||
details: [
|
||||
{
|
||||
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
|
||||
reason: 'ACCESS_DENIED',
|
||||
domain: 'googleapis.com',
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = toFriendlyError(error);
|
||||
expect(result).toBeInstanceOf(ForbiddenError);
|
||||
expect(result).not.toBeInstanceOf(AccountSuspendedError);
|
||||
});
|
||||
|
||||
it('should parse stringified JSON data', () => {
|
||||
const error = {
|
||||
response: {
|
||||
@@ -201,3 +279,50 @@ describe('toFriendlyError', () => {
|
||||
expect(toFriendlyError(error)).toBe(error);
|
||||
});
|
||||
});
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
describe('getErrorType', () => {
|
||||
it('should return error name for standard errors', () => {
|
||||
expect(getErrorType(new Error('test'))).toBe('Error');
|
||||
expect(getErrorType(new TypeError('test'))).toBe('TypeError');
|
||||
expect(getErrorType(new SyntaxError('test'))).toBe('SyntaxError');
|
||||
});
|
||||
|
||||
it('should return constructor name for custom errors', () => {
|
||||
expect(getErrorType(new FatalAuthenticationError('test'))).toBe(
|
||||
'FatalAuthenticationError',
|
||||
);
|
||||
expect(getErrorType(new FatalInputError('test'))).toBe('FatalInputError');
|
||||
expect(getErrorType(new FatalSandboxError('test'))).toBe(
|
||||
'FatalSandboxError',
|
||||
);
|
||||
expect(getErrorType(new FatalConfigError('test'))).toBe('FatalConfigError');
|
||||
expect(getErrorType(new FatalTurnLimitedError('test'))).toBe(
|
||||
'FatalTurnLimitedError',
|
||||
);
|
||||
expect(getErrorType(new FatalToolExecutionError('test'))).toBe(
|
||||
'FatalToolExecutionError',
|
||||
);
|
||||
expect(getErrorType(new FatalCancellationError('test'))).toBe(
|
||||
'FatalCancellationError',
|
||||
);
|
||||
expect(getErrorType(new ForbiddenError('test'))).toBe('ForbiddenError');
|
||||
expect(getErrorType(new AccountSuspendedError('test'))).toBe(
|
||||
'AccountSuspendedError',
|
||||
);
|
||||
expect(getErrorType(new UnauthorizedError('test'))).toBe(
|
||||
'UnauthorizedError',
|
||||
);
|
||||
expect(getErrorType(new BadRequestError('test'))).toBe('BadRequestError');
|
||||
});
|
||||
|
||||
it('should return "unknown" for non-Error objects', () => {
|
||||
expect(getErrorType('string error')).toBe('unknown');
|
||||
expect(getErrorType(123)).toBe('unknown');
|
||||
expect(getErrorType({})).toBe('unknown');
|
||||
expect(getErrorType(null)).toBe('unknown');
|
||||
expect(getErrorType(undefined)).toBe('unknown');
|
||||
});
|
||||
});
|
||||
>>>>>>> ea48bd941 (feat: better error messages (#20577))
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { parseGoogleApiError, type ErrorInfo } from './googleErrors.js';
|
||||
|
||||
interface GaxiosError {
|
||||
response?: {
|
||||
data?: unknown;
|
||||
@@ -79,6 +81,17 @@ export class CanceledError extends Error {
|
||||
}
|
||||
|
||||
export class ForbiddenError extends Error {}
|
||||
export class AccountSuspendedError extends ForbiddenError {
|
||||
readonly appealUrl?: string;
|
||||
readonly appealLinkText?: string;
|
||||
|
||||
constructor(message: string, metadata?: Record<string, string>) {
|
||||
super(message);
|
||||
this.name = 'AccountSuspendedError';
|
||||
this.appealUrl = metadata?.['appeal_url'];
|
||||
this.appealLinkText = metadata?.['appeal_url_link_text'];
|
||||
}
|
||||
}
|
||||
export class UnauthorizedError extends Error {}
|
||||
export class BadRequestError extends Error {}
|
||||
|
||||
@@ -97,10 +110,33 @@ interface ResponseData {
|
||||
}
|
||||
|
||||
export function toFriendlyError(error: unknown): unknown {
|
||||
<<<<<<< HEAD
|
||||
if (error && typeof error === 'object' && 'response' in error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const gaxiosError = error as GaxiosError;
|
||||
const data = parseResponseData(gaxiosError);
|
||||
=======
|
||||
// First, try structured parsing for TOS_VIOLATION detection.
|
||||
const googleApiError = parseGoogleApiError(error);
|
||||
if (googleApiError && googleApiError.code === 403) {
|
||||
const tosDetail = googleApiError.details.find(
|
||||
(d): d is ErrorInfo =>
|
||||
d['@type'] === 'type.googleapis.com/google.rpc.ErrorInfo' &&
|
||||
'reason' in d &&
|
||||
d.reason === 'TOS_VIOLATION',
|
||||
);
|
||||
if (tosDetail) {
|
||||
return new AccountSuspendedError(
|
||||
googleApiError.message,
|
||||
tosDetail.metadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to basic Gaxios error parsing for other HTTP errors.
|
||||
if (isGaxiosError(error)) {
|
||||
const data = parseResponseData(error);
|
||||
>>>>>>> ea48bd941 (feat: better error messages (#20577))
|
||||
if (data && data.error && data.error.message && data.error.code) {
|
||||
switch (data.error.code) {
|
||||
case 400:
|
||||
@@ -108,9 +144,6 @@ export function toFriendlyError(error: unknown): unknown {
|
||||
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:
|
||||
}
|
||||
@@ -119,6 +152,13 @@ export function toFriendlyError(error: unknown): unknown {
|
||||
return error;
|
||||
}
|
||||
|
||||
export function isAccountSuspendedError(
|
||||
error: unknown,
|
||||
): AccountSuspendedError | null {
|
||||
const friendly = toFriendlyError(error);
|
||||
return friendly instanceof AccountSuspendedError ? friendly : null;
|
||||
}
|
||||
|
||||
function parseResponseData(error: GaxiosError): ResponseData | undefined {
|
||||
// Inexplicably, Gaxios sometimes doesn't JSONify the response data.
|
||||
if (typeof error.response?.data === 'string') {
|
||||
|
||||
Reference in New Issue
Block a user