Compare commits

...

2 Commits

Author SHA1 Message Date
A.K.M. Adib 73b4435d75 format file 2026-03-18 15:37:25 -04:00
A.K.M. Adib d62684908f fix(core): treat RESOURCE_EXHAUSTED 429 errors without details as TerminalQuotaError 2026-03-18 14:50:44 -04:00
3 changed files with 35 additions and 1 deletions
+5 -1
View File
@@ -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,
};
}
@@ -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,
@@ -242,6 +242,17 @@ export function classifyGoogleError(error: unknown): unknown {
);
}
// Centralized check for RESOURCE_EXHAUSTED errors
if (
googleApiError?.status === 'RESOURCE_EXHAUSTED' &&
(status === 429 || status === 499)
) {
const errorMessage =
googleApiError?.message ||
(error instanceof Error ? error.message : String(error));
return new TerminalQuotaError(errorMessage, googleApiError);
}
if (
!googleApiError ||
(googleApiError.code !== 429 && googleApiError.code !== 499) ||
@@ -268,6 +279,7 @@ 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).
return new RetryableQuotaError(
errorMessage,
googleApiError ?? {
@@ -405,6 +417,7 @@ export function classifyGoogleError(error: unknown): unknown {
const errorMessage =
googleApiError?.message ||
(error instanceof Error ? error.message : String(error));
return new RetryableQuotaError(
errorMessage,
googleApiError ?? {