From 22e6af414a9c273052bb07facfdaf0fe7543de4f Mon Sep 17 00:00:00 2001 From: Adam Weidman <65992621+adamfweidman@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:14:07 -0800 Subject: [PATCH] feat: attempt more error parsing (#14899) --- packages/core/src/utils/googleErrors.test.ts | 13 ++++- packages/core/src/utils/googleErrors.ts | 52 ++++++++++++------- .../core/src/utils/googleQuotaErrors.test.ts | 48 +++++++++++++++++ packages/core/src/utils/googleQuotaErrors.ts | 12 +++-- 4 files changed, 100 insertions(+), 25 deletions(-) diff --git a/packages/core/src/utils/googleErrors.test.ts b/packages/core/src/utils/googleErrors.test.ts index c051fb0310..46a6aa7b7a 100644 --- a/packages/core/src/utils/googleErrors.test.ts +++ b/packages/core/src/utils/googleErrors.test.ts @@ -94,7 +94,12 @@ describe('parseGoogleApiError', () => { }, }, }; - expect(parseGoogleApiError(mockError)).toBeNull(); + + expect(parseGoogleApiError(mockError)).toEqual({ + code: 400, + message: 'Bad Request', + details: [], + }); }); it('should return null if there are no valid details', () => { @@ -115,7 +120,11 @@ describe('parseGoogleApiError', () => { }, }, }; - expect(parseGoogleApiError(mockError)).toBeNull(); + expect(parseGoogleApiError(mockError)).toEqual({ + code: 400, + message: 'Bad Request', + details: [], + }); }); it('should parse a doubly nested error in the message', () => { diff --git a/packages/core/src/utils/googleErrors.ts b/packages/core/src/utils/googleErrors.ts index d7c15ac0b6..56e20a95cd 100644 --- a/packages/core/src/utils/googleErrors.ts +++ b/packages/core/src/utils/googleErrors.ts @@ -190,32 +190,32 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null { const message = currentError.message; const errorDetails = currentError.details; - if (Array.isArray(errorDetails) && code && message) { + if (code && message) { const details: GoogleApiErrorDetail[] = []; - for (const detail of errorDetails) { - if (detail && typeof detail === 'object') { - const detailObj = detail as Record; - const typeKey = Object.keys(detailObj).find( - (key) => key.trim() === '@type', - ); - if (typeKey) { - if (typeKey !== '@type') { - detailObj['@type'] = detailObj[typeKey]; - delete detailObj[typeKey]; + if (Array.isArray(errorDetails)) { + for (const detail of errorDetails) { + if (detail && typeof detail === 'object') { + const detailObj = detail as Record; + const typeKey = Object.keys(detailObj).find( + (key) => key.trim() === '@type', + ); + if (typeKey) { + if (typeKey !== '@type') { + detailObj['@type'] = detailObj[typeKey]; + delete detailObj[typeKey]; + } + // We can just cast it; the consumer will have to switch on @type + details.push(detailObj as unknown as GoogleApiErrorDetail); } - // We can just cast it; the consumer will have to switch on @type - details.push(detailObj as unknown as GoogleApiErrorDetail); } } } - if (details.length > 0) { - return { - code, - message, - details, - }; - } + return { + code, + message, + details, + }; } return null; @@ -288,6 +288,18 @@ function fromApiError(errorObj: object): ErrorShape | undefined { data = JSON.parse(data); } catch (_) { // Not a JSON string, can't parse. + // Try one more fallback: look for the first '{' and last '}' + if (typeof data === 'string') { + const firstBrace = data.indexOf('{'); + const lastBrace = data.lastIndexOf('}'); + if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) { + try { + data = JSON.parse(data.substring(firstBrace, lastBrace + 1)); + } catch (__) { + // Still failed + } + } + } } } diff --git a/packages/core/src/utils/googleQuotaErrors.test.ts b/packages/core/src/utils/googleQuotaErrors.test.ts index 34836119a8..f6fd9f474c 100644 --- a/packages/core/src/utils/googleQuotaErrors.test.ts +++ b/packages/core/src/utils/googleQuotaErrors.test.ts @@ -12,6 +12,7 @@ import { } from './googleQuotaErrors.js'; import * as errorParser from './googleErrors.js'; import type { GoogleApiError } from './googleErrors.js'; +import { ModelNotFoundError } from './httpErrors.js'; describe('classifyGoogleError', () => { afterEach(() => { @@ -341,4 +342,51 @@ describe('classifyGoogleError', () => { const result = classifyGoogleError(originalError); expect(result).toBe(originalError); }); + + it('should classify nested JSON string 404 error as ModelNotFoundError', () => { + // Mimic the double-wrapped JSON structure seen in the user report + const innerError = { + error: { + code: 404, + message: + 'models/NOT_FOUND is not found for API version v1beta, or is not supported for generateContent. Call ListModels to see the list of available models and their supported methods.', + status: 'NOT_FOUND', + }, + }; + const errorString = JSON.stringify(innerError); + + const outerErrorString = JSON.stringify({ + error: { + message: errorString, + }, + }); + const error = new Error(`[API Error: ${outerErrorString}]`); + + const classified = classifyGoogleError(error); + expect(classified).toBeInstanceOf(ModelNotFoundError); + expect((classified as ModelNotFoundError).code).toBe(404); + }); + + it('should fallback to string parsing for retry delays when details array is empty', () => { + const errorWithEmptyDetails = { + error: { + code: 429, + message: 'Resource exhausted. Please retry in 5s', + details: [], + }, + }; + + const result = classifyGoogleError(errorWithEmptyDetails); + + expect(result).toBeInstanceOf(RetryableQuotaError); + if (result instanceof RetryableQuotaError) { + expect(result.retryDelayMs).toBe(5000); + // The cause should be the parsed GoogleApiError + expect(result.cause).toEqual({ + code: 429, + message: 'Resource exhausted. Please retry in 5s', + details: [], + }); + } + }); }); diff --git a/packages/core/src/utils/googleQuotaErrors.ts b/packages/core/src/utils/googleQuotaErrors.ts index 9cd318ade8..6f315f33e1 100644 --- a/packages/core/src/utils/googleQuotaErrors.ts +++ b/packages/core/src/utils/googleQuotaErrors.ts @@ -89,9 +89,15 @@ export function classifyGoogleError(error: unknown): unknown { return new ModelNotFoundError(message, status); } - if (!googleApiError || googleApiError.code !== 429) { + if ( + !googleApiError || + googleApiError.code !== 429 || + googleApiError.details.length === 0 + ) { // Fallback: try to parse the error message for a retry delay - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = + googleApiError?.message || + (error instanceof Error ? error.message : String(error)); const match = errorMessage.match(/Please retry in ([0-9.]+(?:ms|s))/); if (match?.[1]) { const retryDelaySeconds = parseDurationInSeconds(match[1]); @@ -108,7 +114,7 @@ export function classifyGoogleError(error: unknown): unknown { } } - return error; // Not a 429 error we can handle. + return error; // Not a 429 error we can handle with structured details or a parsable retry message. } const quotaFailure = googleApiError.details.find(