mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-12 22:31:12 -07:00
fix: decode Uint8Array and multi-byte UTF-8 in API error messages (#23341)
Co-authored-by: Coco Sheng <cocosheng@google.com>
This commit is contained in:
@@ -315,6 +315,100 @@ describe('LoggingContentGenerator', () => {
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should decode Uint8Array data in Gaxios errors', async () => {
|
||||
const req = { contents: [], model: 'gemini-pro' };
|
||||
|
||||
const gaxiosError = Object.assign(new Error('Gaxios Error'), {
|
||||
response: { data: new Uint8Array([72, 101, 108, 108, 111]) },
|
||||
});
|
||||
|
||||
vi.mocked(wrapped.generateContent).mockRejectedValue(gaxiosError);
|
||||
|
||||
await expect(
|
||||
loggingContentGenerator.generateContent(
|
||||
req,
|
||||
'prompt-123',
|
||||
LlmRole.MAIN,
|
||||
),
|
||||
).rejects.toSatisfy((error: unknown) => {
|
||||
const gError = error as { response: { data: unknown } };
|
||||
expect(gError.response.data).toBe('Hello');
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should decode multi-byte UTF-8 from comma-separated byte strings', async () => {
|
||||
const req = { contents: [], model: 'gemini-pro' };
|
||||
|
||||
// "Héllo" in UTF-8 bytes: H=72, é=195,169, l=108, l=108, o=111
|
||||
const utf8Data = '72,195,169,108,108,111';
|
||||
const gaxiosError = Object.assign(new Error('Gaxios Error'), {
|
||||
response: { data: utf8Data },
|
||||
});
|
||||
|
||||
vi.mocked(wrapped.generateContent).mockRejectedValue(gaxiosError);
|
||||
|
||||
await expect(
|
||||
loggingContentGenerator.generateContent(
|
||||
req,
|
||||
'prompt-123',
|
||||
LlmRole.MAIN,
|
||||
),
|
||||
).rejects.toSatisfy((error: unknown) => {
|
||||
const gError = error as { response: { data: unknown } };
|
||||
expect(gError.response.data).toBe('Héllo');
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should decode 3-byte UTF-8 from comma-separated byte strings', async () => {
|
||||
const req = { contents: [], model: 'gemini-pro' };
|
||||
|
||||
// "こんにちは" in UTF-8 bytes (3 bytes per character)
|
||||
const utf8Data =
|
||||
'227,129,147,227,130,147,227,129,171,227,129,161,227,129,175';
|
||||
const gaxiosError = Object.assign(new Error('Gaxios Error'), {
|
||||
response: { data: utf8Data },
|
||||
});
|
||||
|
||||
vi.mocked(wrapped.generateContent).mockRejectedValue(gaxiosError);
|
||||
|
||||
await expect(
|
||||
loggingContentGenerator.generateContent(
|
||||
req,
|
||||
'prompt-123',
|
||||
LlmRole.MAIN,
|
||||
),
|
||||
).rejects.toSatisfy((error: unknown) => {
|
||||
const gError = error as { response: { data: unknown } };
|
||||
expect(gError.response.data).toBe('こんにちは');
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject byte strings with values outside 0-255 range', async () => {
|
||||
const req = { contents: [], model: 'gemini-pro' };
|
||||
|
||||
const outOfRange = '72,256,108';
|
||||
const gaxiosError = Object.assign(new Error('Gaxios Error'), {
|
||||
response: { data: outOfRange },
|
||||
});
|
||||
|
||||
vi.mocked(wrapped.generateContent).mockRejectedValue(gaxiosError);
|
||||
|
||||
await expect(
|
||||
loggingContentGenerator.generateContent(
|
||||
req,
|
||||
'prompt-123',
|
||||
LlmRole.MAIN,
|
||||
),
|
||||
).rejects.toSatisfy((error: unknown) => {
|
||||
const gError = error as { response: { data: unknown } };
|
||||
expect(gError.response.data).toBe(outOfRange);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT log error on AbortError (user cancellation)', async () => {
|
||||
|
||||
@@ -276,8 +276,10 @@ export class LoggingContentGenerator implements ContentGenerator {
|
||||
}
|
||||
|
||||
private _fixGaxiosErrorData(error: unknown): void {
|
||||
// Fix for raw ASCII buffer strings appearing in dev with the latest
|
||||
// Gaxios updates.
|
||||
// Fix for raw buffer data appearing in Gaxios errors.
|
||||
// Gaxios may return the response body as a Uint8Array, a Buffer, or
|
||||
// a string of comma-separated byte values (e.g. "72,101,108,108,111").
|
||||
// All three forms need to be decoded as UTF-8.
|
||||
if (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
@@ -288,11 +290,20 @@ export class LoggingContentGenerator implements ContentGenerator {
|
||||
) {
|
||||
const response = error.response as { data: unknown };
|
||||
const data = response.data;
|
||||
if (typeof data === 'string' && data.includes(',')) {
|
||||
|
||||
if (data instanceof Uint8Array) {
|
||||
// Gaxios returned raw bytes directly
|
||||
response.data = new TextDecoder().decode(data);
|
||||
} else if (typeof data === 'string' && data.includes(',')) {
|
||||
// Gaxios returned bytes as a comma-separated string
|
||||
try {
|
||||
const charCodes = data.split(',').map(Number);
|
||||
if (charCodes.every((code) => !isNaN(code))) {
|
||||
response.data = String.fromCharCode(...charCodes);
|
||||
const byteValues = data.split(',').map(Number);
|
||||
if (
|
||||
byteValues.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)
|
||||
) {
|
||||
response.data = new TextDecoder().decode(
|
||||
new Uint8Array(byteValues),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, just leave it alone
|
||||
|
||||
Reference in New Issue
Block a user