feat: try more parsing

This commit is contained in:
Adam Weidman
2025-12-10 12:02:30 -05:00
committed by Tommaso Sciortino
parent a8e3928dd2
commit c3f6e7132b
4 changed files with 100 additions and 25 deletions
+11 -2
View File
@@ -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', () => { 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', () => { it('should parse a doubly nested error in the message', () => {
+32 -20
View File
@@ -190,32 +190,32 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null {
const message = currentError.message; const message = currentError.message;
const errorDetails = currentError.details; const errorDetails = currentError.details;
if (Array.isArray(errorDetails) && code && message) { if (code && message) {
const details: GoogleApiErrorDetail[] = []; const details: GoogleApiErrorDetail[] = [];
for (const detail of errorDetails) { if (Array.isArray(errorDetails)) {
if (detail && typeof detail === 'object') { for (const detail of errorDetails) {
const detailObj = detail as Record<string, unknown>; if (detail && typeof detail === 'object') {
const typeKey = Object.keys(detailObj).find( const detailObj = detail as Record<string, unknown>;
(key) => key.trim() === '@type', const typeKey = Object.keys(detailObj).find(
); (key) => key.trim() === '@type',
if (typeKey) { );
if (typeKey !== '@type') { if (typeKey) {
detailObj['@type'] = detailObj[typeKey]; if (typeKey !== '@type') {
delete detailObj[typeKey]; 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 {
return { code,
code, message,
message, details,
details, };
};
}
} }
return null; return null;
@@ -288,6 +288,18 @@ function fromApiError(errorObj: object): ErrorShape | undefined {
data = JSON.parse(data); data = JSON.parse(data);
} catch (_) { } catch (_) {
// Not a JSON string, can't parse. // 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
}
}
}
} }
} }
@@ -12,6 +12,7 @@ import {
} from './googleQuotaErrors.js'; } from './googleQuotaErrors.js';
import * as errorParser from './googleErrors.js'; import * as errorParser from './googleErrors.js';
import type { GoogleApiError } from './googleErrors.js'; import type { GoogleApiError } from './googleErrors.js';
import { ModelNotFoundError } from './httpErrors.js';
describe('classifyGoogleError', () => { describe('classifyGoogleError', () => {
afterEach(() => { afterEach(() => {
@@ -341,4 +342,51 @@ describe('classifyGoogleError', () => {
const result = classifyGoogleError(originalError); const result = classifyGoogleError(originalError);
expect(result).toBe(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: [],
});
}
});
}); });
+9 -3
View File
@@ -89,9 +89,15 @@ export function classifyGoogleError(error: unknown): unknown {
return new ModelNotFoundError(message, status); 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 // 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))/); const match = errorMessage.match(/Please retry in ([0-9.]+(?:ms|s))/);
if (match?.[1]) { if (match?.[1]) {
const retryDelaySeconds = parseDurationInSeconds(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( const quotaFailure = googleApiError.details.find(