From 73ae60e4cd2494975b9394ad9d817e7160fe3551 Mon Sep 17 00:00:00 2001 From: Jerry Lin Date: Fri, 26 Jun 2026 21:00:09 -0700 Subject: [PATCH 1/7] fix(gaxios): prevent comma corruption when draining stream error responses --- core/packages/gaxios/src/gaxios.ts | 8 ++++++- core/packages/gaxios/test/test.getch.ts | 31 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/core/packages/gaxios/src/gaxios.ts b/core/packages/gaxios/src/gaxios.ts index 2df23b4c07b6..6edf236ff0d5 100644 --- a/core/packages/gaxios/src/gaxios.ts +++ b/core/packages/gaxios/src/gaxios.ts @@ -202,7 +202,13 @@ export class Gaxios implements FetchCompliance { response.push(chunk); } - translatedResponse.data = response.toString() as T; + if (response.every(chunk => typeof chunk === 'string')) { + translatedResponse.data = response.join('') as T; + } else { + translatedResponse.data = Buffer.concat( + response.map(c => (typeof c === 'string' ? Buffer.from(c) : c)), + ).toString('utf8') as T; + } } const errorInfo = GaxiosError.extractAPIErrorFromResponse( diff --git a/core/packages/gaxios/test/test.getch.ts b/core/packages/gaxios/test/test.getch.ts index 3db1a3d46fce..171d4fc40946 100644 --- a/core/packages/gaxios/test/test.getch.ts +++ b/core/packages/gaxios/test/test.getch.ts @@ -143,6 +143,29 @@ describe('🚙 error handling', () => { ); }); + it('should handle stream error responses split across multiple chunks without corrupting them with commas', async () => { + const chunks = [ + '{"error": {"code": 400, ', + '"message": "Invalid ', + 'argument", "status": "INVALID_ARGUMENT"}}', + ]; + const readableStream = Readable.from(chunks); + const scope = nock(url).get('/').reply(400, readableStream); + + await assert.rejects( + request({url, responseType: 'stream'}), + (err: GaxiosError) => { + scope.done(); + const apiError = JSON.parse(err.message); + return ( + apiError.error.code === 400 && + apiError.error.message === 'Invalid argument' && + apiError.error.status === 'INVALID_ARGUMENT' + ); + }, + ); + }); + it('should not throw an error during a translation error', () => { const notJSON = '.'; const response = { @@ -1302,6 +1325,14 @@ describe('🍂 defaults & instances', () => { }); describe('mtls', () => { + beforeEach(() => { + setEnv({ + HTTP_PROXY: undefined, + HTTPS_PROXY: undefined, + http_proxy: undefined, + https_proxy: undefined, + }); + }); class GaxiosAssertAgentCache extends Gaxios { getAgentCache() { return this.agentCache; From ff71f844209a41f4e42c9bf044892ef85eb39246 Mon Sep 17 00:00:00 2001 From: Gabe Pearhill Date: Mon, 29 Jun 2026 14:05:35 -0700 Subject: [PATCH 2/7] remove unecessary env var configuration --- core/packages/gaxios/test/test.getch.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/core/packages/gaxios/test/test.getch.ts b/core/packages/gaxios/test/test.getch.ts index 171d4fc40946..531473d30742 100644 --- a/core/packages/gaxios/test/test.getch.ts +++ b/core/packages/gaxios/test/test.getch.ts @@ -1325,14 +1325,6 @@ describe('🍂 defaults & instances', () => { }); describe('mtls', () => { - beforeEach(() => { - setEnv({ - HTTP_PROXY: undefined, - HTTPS_PROXY: undefined, - http_proxy: undefined, - https_proxy: undefined, - }); - }); class GaxiosAssertAgentCache extends Gaxios { getAgentCache() { return this.agentCache; From 40e8de3366a56f6f256b42f0811fb2ff14b4935c Mon Sep 17 00:00:00 2001 From: Gabe Pearhill Date: Mon, 29 Jun 2026 14:14:33 -0700 Subject: [PATCH 3/7] add test that verifies buffer behavior --- core/packages/gaxios/test/test.getch.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/core/packages/gaxios/test/test.getch.ts b/core/packages/gaxios/test/test.getch.ts index 531473d30742..6ff8095507a1 100644 --- a/core/packages/gaxios/test/test.getch.ts +++ b/core/packages/gaxios/test/test.getch.ts @@ -166,6 +166,29 @@ describe('🚙 error handling', () => { ); }); + it('should handle stream error responses split across multiple Buffer chunks without corrupting them with commas', async () => { + const chunks = [ + Buffer.from('{"error": {"code": 400, '), + Buffer.from('"message": "Invalid '), + Buffer.from('argument", "status": "INVALID_ARGUMENT"}}'), + ]; + const readableStream = Readable.from(chunks); + const scope = nock(url).get('/').reply(400, readableStream); + + await assert.rejects( + request({url, responseType: 'stream'}), + (err: GaxiosError) => { + scope.done(); + const apiError = JSON.parse(err.message); + return ( + apiError.error.code === 400 && + apiError.error.message === 'Invalid argument' && + apiError.error.status === 'INVALID_ARGUMENT' + ); + }, + ); + }); + it('should not throw an error during a translation error', () => { const notJSON = '.'; const response = { From f358492979878fd4869f1505d093000529f0dc1b Mon Sep 17 00:00:00 2001 From: Gabe Pearhill Date: Mon, 29 Jun 2026 14:19:45 -0700 Subject: [PATCH 4/7] add unit tests for Uint8Array and mixed type streams --- core/packages/gaxios/test/test.getch.ts | 45 +++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/core/packages/gaxios/test/test.getch.ts b/core/packages/gaxios/test/test.getch.ts index 6ff8095507a1..98babeea29ca 100644 --- a/core/packages/gaxios/test/test.getch.ts +++ b/core/packages/gaxios/test/test.getch.ts @@ -189,6 +189,51 @@ describe('🚙 error handling', () => { ); }); + it('should handle stream error responses with Uint8Array chunks without corrupting them', async () => { + const chunks = [ + new Uint8Array(Buffer.from('{"error": {"code": 400, ')), + new Uint8Array(Buffer.from('"message": "Invalid ')), + new Uint8Array(Buffer.from('argument", "status": "INVALID_ARGUMENT"}}')), + ]; + const readableStream = Readable.from(chunks); + const scope = nock(url).get('/').reply(400, readableStream); + + await assert.rejects( + request({url, responseType: 'stream'}), + (err: GaxiosError) => { + scope.done(); + const apiError = JSON.parse(err.message); + return ( + apiError.error.code === 400 && + apiError.error.message === 'Invalid argument' && + apiError.error.status === 'INVALID_ARGUMENT' + ); + }, + ); + }); + + it('should handle stream error responses with mixed chunks (string, Buffer, Uint8Array) without corrupting them', async () => { + const chunks = [ + '{"error": {"code": 400, ', + Buffer.from('"message": "Invalid '), + new Uint8Array(Buffer.from('argument", "status": "INVALID_ARGUMENT"}}')), + ]; + const readableStream = Readable.from(chunks); + const scope = nock(url).get('/').reply(400, readableStream); + await assert.rejects( + request({url, responseType: 'stream'}), + (err: GaxiosError) => { + scope.done(); + const apiError = JSON.parse(err.message); + return ( + apiError.error.code === 400 && + apiError.error.message === 'Invalid argument' && + apiError.error.status === 'INVALID_ARGUMENT' + ); + }, + ); + }); + it('should not throw an error during a translation error', () => { const notJSON = '.'; const response = { From daf41e6c66f021cdd0e8da6822450ca73dcdc7b1 Mon Sep 17 00:00:00 2001 From: Gabe Pearhill Date: Mon, 29 Jun 2026 14:23:22 -0700 Subject: [PATCH 5/7] improve test layout and verbiage --- core/packages/gaxios/test/test.getch.ts | 169 ++++++++++++------------ 1 file changed, 86 insertions(+), 83 deletions(-) diff --git a/core/packages/gaxios/test/test.getch.ts b/core/packages/gaxios/test/test.getch.ts index 98babeea29ca..b70c65c3c9df 100644 --- a/core/packages/gaxios/test/test.getch.ts +++ b/core/packages/gaxios/test/test.getch.ts @@ -143,95 +143,98 @@ describe('🚙 error handling', () => { ); }); - it('should handle stream error responses split across multiple chunks without corrupting them with commas', async () => { - const chunks = [ - '{"error": {"code": 400, ', - '"message": "Invalid ', - 'argument", "status": "INVALID_ARGUMENT"}}', - ]; - const readableStream = Readable.from(chunks); - const scope = nock(url).get('/').reply(400, readableStream); + describe('should handle stream error responses', () => { + it('split across multiple string chunks', async () => { + const chunks = [ + '{"error": {"code": 400, ', + '"message": "Invalid ', + 'argument", "status": "INVALID_ARGUMENT"}}', + ]; + const readableStream = Readable.from(chunks); + const scope = nock(url).get('/').reply(400, readableStream); - await assert.rejects( - request({url, responseType: 'stream'}), - (err: GaxiosError) => { - scope.done(); - const apiError = JSON.parse(err.message); - return ( - apiError.error.code === 400 && - apiError.error.message === 'Invalid argument' && - apiError.error.status === 'INVALID_ARGUMENT' - ); - }, - ); - }); + await assert.rejects( + request({url, responseType: 'stream'}), + (err: GaxiosError) => { + scope.done(); + const apiError = JSON.parse(err.message); + return ( + apiError.error.code === 400 && + apiError.error.message === 'Invalid argument' && + apiError.error.status === 'INVALID_ARGUMENT' + ); + }, + ); + }); - it('should handle stream error responses split across multiple Buffer chunks without corrupting them with commas', async () => { - const chunks = [ - Buffer.from('{"error": {"code": 400, '), - Buffer.from('"message": "Invalid '), - Buffer.from('argument", "status": "INVALID_ARGUMENT"}}'), - ]; - const readableStream = Readable.from(chunks); - const scope = nock(url).get('/').reply(400, readableStream); + it('split across multiple Buffer chunks', async () => { + const chunks = [ + Buffer.from('{"error": {"code": 400, '), + Buffer.from('"message": "Invalid '), + Buffer.from('argument", "status": "INVALID_ARGUMENT"}}'), + ]; + const readableStream = Readable.from(chunks); + const scope = nock(url).get('/').reply(400, readableStream); - await assert.rejects( - request({url, responseType: 'stream'}), - (err: GaxiosError) => { - scope.done(); - const apiError = JSON.parse(err.message); - return ( - apiError.error.code === 400 && - apiError.error.message === 'Invalid argument' && - apiError.error.status === 'INVALID_ARGUMENT' - ); - }, - ); - }); + await assert.rejects( + request({url, responseType: 'stream'}), + (err: GaxiosError) => { + scope.done(); + const apiError = JSON.parse(err.message); + return ( + apiError.error.code === 400 && + apiError.error.message === 'Invalid argument' && + apiError.error.status === 'INVALID_ARGUMENT' + ); + }, + ); + }); - it('should handle stream error responses with Uint8Array chunks without corrupting them', async () => { - const chunks = [ - new Uint8Array(Buffer.from('{"error": {"code": 400, ')), - new Uint8Array(Buffer.from('"message": "Invalid ')), - new Uint8Array(Buffer.from('argument", "status": "INVALID_ARGUMENT"}}')), - ]; - const readableStream = Readable.from(chunks); - const scope = nock(url).get('/').reply(400, readableStream); + it('split across multiple Uint8Array chunks', async () => { + const chunks = [ + new Uint8Array(Buffer.from('{"error": {"code": 400, ')), + new Uint8Array(Buffer.from('"message": "Invalid ')), + new Uint8Array(Buffer.from('argument", "status": "INVALID_ARGUMENT"}}')), + ]; + const readableStream = Readable.from(chunks); + const scope = nock(url).get('/').reply(400, readableStream); - await assert.rejects( - request({url, responseType: 'stream'}), - (err: GaxiosError) => { - scope.done(); - const apiError = JSON.parse(err.message); - return ( - apiError.error.code === 400 && - apiError.error.message === 'Invalid argument' && - apiError.error.status === 'INVALID_ARGUMENT' - ); - }, - ); - }); + await assert.rejects( + request({url, responseType: 'stream'}), + (err: GaxiosError) => { + scope.done(); + const apiError = JSON.parse(err.message); + return ( + apiError.error.code === 400 && + apiError.error.message === 'Invalid argument' && + apiError.error.status === 'INVALID_ARGUMENT' + ); + }, + ); + }); - it('should handle stream error responses with mixed chunks (string, Buffer, Uint8Array) without corrupting them', async () => { - const chunks = [ - '{"error": {"code": 400, ', - Buffer.from('"message": "Invalid '), - new Uint8Array(Buffer.from('argument", "status": "INVALID_ARGUMENT"}}')), - ]; - const readableStream = Readable.from(chunks); - const scope = nock(url).get('/').reply(400, readableStream); - await assert.rejects( - request({url, responseType: 'stream'}), - (err: GaxiosError) => { - scope.done(); - const apiError = JSON.parse(err.message); - return ( - apiError.error.code === 400 && - apiError.error.message === 'Invalid argument' && - apiError.error.status === 'INVALID_ARGUMENT' - ); - }, - ); + it('split accross mixed type chunks (string, Buffer, Uint8Array)', async () => { + const chunks = [ + '{"error": {"code": 400, ', + Buffer.from('"message": "Invalid '), + new Uint8Array(Buffer.from('argument", "status": "INVALID_ARGUMENT"}}')), + ]; + const readableStream = Readable.from(chunks); + const scope = nock(url).get('/').reply(400, readableStream); + + await assert.rejects( + request({url, responseType: 'stream'}), + (err: GaxiosError) => { + scope.done(); + const apiError = JSON.parse(err.message); + return ( + apiError.error.code === 400 && + apiError.error.message === 'Invalid argument' && + apiError.error.status === 'INVALID_ARGUMENT' + ); + }, + ); + }); }); it('should not throw an error during a translation error', () => { From baaef56a984d60bfbf29323a2ec652c965735b2f Mon Sep 17 00:00:00 2001 From: Gabe Pearhill Date: Mon, 29 Jun 2026 14:26:19 -0700 Subject: [PATCH 6/7] fix type in test message --- core/packages/gaxios/test/test.getch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/packages/gaxios/test/test.getch.ts b/core/packages/gaxios/test/test.getch.ts index b70c65c3c9df..62072b7897c8 100644 --- a/core/packages/gaxios/test/test.getch.ts +++ b/core/packages/gaxios/test/test.getch.ts @@ -213,7 +213,7 @@ describe('🚙 error handling', () => { ); }); - it('split accross mixed type chunks (string, Buffer, Uint8Array)', async () => { + it('split across mixed type chunks (string, Buffer, Uint8Array)', async () => { const chunks = [ '{"error": {"code": 400, ', Buffer.from('"message": "Invalid '), From 85cca1ee5e3d079558e84084807bcce9587736cc Mon Sep 17 00:00:00 2001 From: Gabe Pearhill Date: Mon, 29 Jun 2026 14:34:30 -0700 Subject: [PATCH 7/7] remove unecessary optimization --- core/packages/gaxios/src/gaxios.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/core/packages/gaxios/src/gaxios.ts b/core/packages/gaxios/src/gaxios.ts index 6edf236ff0d5..c2854de06661 100644 --- a/core/packages/gaxios/src/gaxios.ts +++ b/core/packages/gaxios/src/gaxios.ts @@ -202,13 +202,9 @@ export class Gaxios implements FetchCompliance { response.push(chunk); } - if (response.every(chunk => typeof chunk === 'string')) { - translatedResponse.data = response.join('') as T; - } else { - translatedResponse.data = Buffer.concat( - response.map(c => (typeof c === 'string' ? Buffer.from(c) : c)), - ).toString('utf8') as T; - } + translatedResponse.data = Buffer.concat( + response.map(c => (typeof c === 'string' ? Buffer.from(c) : c)), + ).toString('utf8') as T; } const errorInfo = GaxiosError.extractAPIErrorFromResponse(