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:
June
2026-04-09 18:06:54 -07:00
committed by GitHub
parent 96cc8a0dad
commit 69bf2d75ef
2 changed files with 111 additions and 6 deletions

View File

@@ -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 () => {

View File

@@ -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