From d62684908f99fa02bea804c1f1f43f662b210122 Mon Sep 17 00:00:00 2001 From: "A.K.M. Adib" Date: Wed, 18 Mar 2026 14:50:44 -0400 Subject: [PATCH] fix(core): treat RESOURCE_EXHAUSTED 429 errors without details as TerminalQuotaError --- packages/core/src/utils/googleErrors.ts | 6 +++++- .../core/src/utils/googleQuotaErrors.test.ts | 17 +++++++++++++++++ packages/core/src/utils/googleQuotaErrors.ts | 12 ++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/core/src/utils/googleErrors.ts b/packages/core/src/utils/googleErrors.ts index 4439d55de5..231ff7993d 100644 --- a/packages/core/src/utils/googleErrors.ts +++ b/packages/core/src/utils/googleErrors.ts @@ -128,12 +128,14 @@ export interface GoogleApiError { code: number; message: string; details: GoogleApiErrorDetail[]; + status?: string; } type ErrorShape = { message?: string; details?: unknown[]; code?: number; + status?: string; }; /** @@ -213,6 +215,7 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null { const code = currentError.code; const message = currentError.message; const errorDetails = currentError.details; + const status = currentError.status; if (code && message) { const details: GoogleApiErrorDetail[] = []; @@ -231,7 +234,7 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null { } // Basic structural check before casting. // Since the proto definitions are loose, we primarily rely on @type presence. - // eslint-disable-next-line no-restricted-syntax + if (typeof detailObj['@type'] === 'string') { // We can just cast it; the consumer will have to switch on @type // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion @@ -246,6 +249,7 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null { code, message, details, + status, }; } diff --git a/packages/core/src/utils/googleQuotaErrors.test.ts b/packages/core/src/utils/googleQuotaErrors.test.ts index 90769def35..ec699f4ecc 100644 --- a/packages/core/src/utils/googleQuotaErrors.test.ts +++ b/packages/core/src/utils/googleQuotaErrors.test.ts @@ -81,6 +81,23 @@ describe('classifyGoogleError', () => { } }); + it('should return TerminalQuotaError for 429 when status is RESOURCE_EXHAUSTED and details are empty', () => { + const apiError: GoogleApiError = { + code: 429, + status: 'RESOURCE_EXHAUSTED', + message: 'Your prepayment funds are depleted.', + details: [], + }; + vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError); + const originalError = new Error('Your prepayment funds are depleted.'); + const result = classifyGoogleError(originalError); + expect(result).toBeInstanceOf(TerminalQuotaError); + if (result instanceof TerminalQuotaError) { + expect(result.cause).toBe(apiError); + expect(result.message).toBe('Your prepayment funds are depleted.'); + } + }); + it('should return original error if code is not 429, 499 or 503', () => { const apiError: GoogleApiError = { code: 500, diff --git a/packages/core/src/utils/googleQuotaErrors.ts b/packages/core/src/utils/googleQuotaErrors.ts index 5a0bf48092..c523581279 100644 --- a/packages/core/src/utils/googleQuotaErrors.ts +++ b/packages/core/src/utils/googleQuotaErrors.ts @@ -268,6 +268,13 @@ export function classifyGoogleError(error: unknown): unknown { } else if (status === 429 || status === 499) { // Fallback: If it is a 429 or 499 but doesn't have a specific "retry in" message, // assume it is a temporary rate limit and retry after 5 sec (same as DEFAULT_RETRY_OPTIONS). + + // However, if the API explicitly returns RESOURCE_EXHAUSTED without details, + // it indicates a hard quota exhaustion rather than a transient rate limit. + if (googleApiError?.status === 'RESOURCE_EXHAUSTED') { + return new TerminalQuotaError(errorMessage, googleApiError); + } + return new RetryableQuotaError( errorMessage, googleApiError ?? { @@ -405,6 +412,11 @@ export function classifyGoogleError(error: unknown): unknown { const errorMessage = googleApiError?.message || (error instanceof Error ? error.message : String(error)); + + if (googleApiError?.status === 'RESOURCE_EXHAUSTED') { + return new TerminalQuotaError(errorMessage, googleApiError); + } + return new RetryableQuotaError( errorMessage, googleApiError ?? {