Files
gemini-cli/packages/core/src/utils/googleErrors.ts

243 lines
5.7 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview
* This file contains types and functions for parsing structured Google API errors.
*/
/**
* Based on google/rpc/error_details.proto
*/
export interface ErrorInfo {
'@type': 'type.googleapis.com/google.rpc.ErrorInfo';
reason: string;
domain: string;
metadata: { [key: string]: string };
}
export interface RetryInfo {
'@type': 'type.googleapis.com/google.rpc.RetryInfo';
retryDelay: string; // e.g. "51820.638305887s"
}
export interface DebugInfo {
'@type': 'type.googleapis.com/google.rpc.DebugInfo';
stackEntries: string[];
detail: string;
}
export interface QuotaFailure {
'@type': 'type.googleapis.com/google.rpc.QuotaFailure';
violations: Array<{
subject: string;
description: string;
apiService?: string;
quotaMetric?: string;
quotaId?: string;
quotaDimensions?: { [key: string]: string };
quotaValue?: number;
futureQuotaValue?: number;
}>;
}
export interface PreconditionFailure {
'@type': 'type.googleapis.com/google.rpc.PreconditionFailure';
violations: Array<{
type: string;
subject: string;
description: string;
}>;
}
export interface LocalizedMessage {
'@type': 'type.googleapis.com/google.rpc.LocalizedMessage';
locale: string;
message: string;
}
export interface BadRequest {
'@type': 'type.googleapis.com/google.rpc.BadRequest';
fieldViolations: Array<{
field: string;
description: string;
reason?: string;
localizedMessage?: LocalizedMessage;
}>;
}
export interface RequestInfo {
'@type': 'type.googleapis.com/google.rpc.RequestInfo';
requestId: string;
servingData: string;
}
export interface ResourceInfo {
'@type': 'type.googleapis.com/google.rpc.ResourceInfo';
resourceType: string;
resourceName: string;
owner: string;
description: string;
}
export interface Help {
'@type': 'type.googleapis.com/google.rpc.Help';
links: Array<{
description: string;
url: string;
}>;
}
export type GoogleApiErrorDetail =
| ErrorInfo
| RetryInfo
| DebugInfo
| QuotaFailure
| PreconditionFailure
| BadRequest
| RequestInfo
| ResourceInfo
| Help
| LocalizedMessage;
export interface GoogleApiError {
code: number;
message: string;
details: GoogleApiErrorDetail[];
}
/**
* Parses an error object to check if it's a structured Google API error
* and extracts all details.
*
* This function can handle two formats:
* 1. Standard Google API errors where `details` is a top-level field.
* 2. Errors where the entire structured error object is stringified inside
* the `message` field of a wrapper error.
*
* @param error The error object to inspect.
* @returns A GoogleApiError object if the error matches, otherwise null.
*/
export function parseGoogleApiError(error: unknown): GoogleApiError | null {
if (!error) {
return null;
}
let errorObj: unknown = error;
// If error is a string, try to parse it.
if (typeof errorObj === 'string') {
try {
errorObj = JSON.parse(errorObj);
} catch (_) {
// Not a JSON string, can't parse.
return null;
}
}
if (typeof errorObj !== 'object' || errorObj === null) {
return null;
}
type ErrorShape = {
message?: string;
details?: unknown[];
code?: number;
};
const gaxiosError = errorObj as {
response?: {
status?: number;
data?:
| {
error?: ErrorShape;
}
| string;
};
error?: ErrorShape;
code?: number;
};
let outerError: ErrorShape | undefined;
if (gaxiosError.response?.data) {
if (typeof gaxiosError.response.data === 'string') {
try {
const parsedData = JSON.parse(gaxiosError.response.data);
if (parsedData.error) {
outerError = parsedData.error;
}
} catch (_) {
// Not a JSON string, or doesn't contain .error
}
} else if (
typeof gaxiosError.response.data === 'object' &&
gaxiosError.response.data !== null
) {
outerError = (
gaxiosError.response.data as {
error?: ErrorShape;
}
).error;
}
}
const responseStatus = gaxiosError.response?.status;
if (!outerError) {
// If the gaxios structure isn't there, check for a top-level `error` property.
if (gaxiosError.error) {
outerError = gaxiosError.error;
} else {
return null;
}
}
let currentError = outerError;
let depth = 0;
const maxDepth = 10;
// Handle cases where the actual error object is stringified inside the message
// by drilling down until we find an error that doesn't have a stringified message.
while (typeof currentError.message === 'string' && depth < maxDepth) {
try {
const parsedMessage = JSON.parse(currentError.message);
if (parsedMessage.error) {
currentError = parsedMessage.error;
depth++;
} else {
// The message is a JSON string, but not a nested error object.
break;
}
} catch (_) {
// It wasn't a JSON string, so we've drilled down as far as we can.
break;
}
}
const code = responseStatus ?? currentError.code ?? gaxiosError.code;
const message = currentError.message;
const errorDetails = currentError.details;
if (Array.isArray(errorDetails) && code && message) {
const details: GoogleApiErrorDetail[] = [];
for (const detail of errorDetails) {
if (detail && typeof detail === 'object' && '@type' in detail) {
// We can just cast it; the consumer will have to switch on @type
details.push(detail as GoogleApiErrorDetail);
}
}
if (details.length > 0) {
return {
code,
message,
details,
};
}
}
return null;
}