From 85a9f33fa9c45cba38a9813dacca5d6fd28acf4d Mon Sep 17 00:00:00 2001 From: bcoe Date: Fri, 26 Feb 2021 10:57:52 -0800 Subject: [PATCH 1/3] feat: handle application/x-www-form-urlencoded/Buffer --- src/gaxios.ts | 41 +++++++++++++++++++++++++++++------------ test/test.getch.ts | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/src/gaxios.ts b/src/gaxios.ts index de13c585..053129e4 100644 --- a/src/gaxios.ts +++ b/src/gaxios.ts @@ -40,6 +40,19 @@ function hasFetch() { return hasWindow() && !!window.fetch; } +function hasBuffer() { + return typeof Buffer !== 'undefined'; +} + +function hasHeader(options: GaxiosOptions, headerCheck: string) { + headerCheck = headerCheck.toLowerCase(); + for (let header of Object.keys(options?.headers || {})) { + header = header.toLowerCase(); + if (header === headerCheck) return true; + } + return false; +} + let HttpsProxyAgent: any; function loadProxy() { @@ -219,21 +232,25 @@ export class Gaxios { if (opts.data) { if (isStream.readable(opts.data)) { opts.body = opts.data; + } else if (hasBuffer() && Buffer.isBuffer(opts.data)) { + // Do not attempt to JSON.stringify() a Buffer: + opts.body = opts.data; + if (!hasHeader(opts, 'Content-Type')) { + opts.headers['Content-Type'] = 'application/json'; + } } else if (typeof opts.data === 'object') { - opts.body = JSON.stringify(opts.data); - // Allow the user to specifiy their own content type, - // such as application/json-patch+json; for historical reasons this - // content type must currently be a json type, as we are relying on - // application/x-www-form-urlencoded (which is incompatible with - // upstream GCP APIs) being rewritten to application/json. - // - // TODO: refactor upstream dependencies to stop relying on this - // side-effect. + // If www-form-urlencoded content type has been set, but data is + // provided as an object, serialize the content using querystring: if ( - !opts.headers['Content-Type'] || - !opts.headers['Content-Type'].includes('json') + opts.headers['Content-Type'] && + opts.headers['Content-Type'] === 'application/x-www-form-urlencoded' ) { - opts.headers['Content-Type'] = 'application/json'; + opts.body = opts.paramsSerializer(opts.data); + } else { + if (!hasHeader(opts, 'Content-Type')) { + opts.headers['Content-Type'] = 'application/json'; + } + opts.body = JSON.stringify(opts.data); } } else { opts.body = opts.data; diff --git a/test/test.getch.ts b/test/test.getch.ts index 847f9331..f47cdfa6 100644 --- a/test/test.getch.ts +++ b/test/test.getch.ts @@ -449,11 +449,11 @@ describe('🎏 data handling', () => { assert.deepStrictEqual(res.data, {}); }); - it('replaces application/x-www-form-urlencoded with application/json', async () => { + it('application/x-www-form-urlencoded with object data should stringify with qs', async () => { const body = {hello: '🌎'}; const scope = nock(url) - .matchHeader('Content-Type', 'application/json') - .post('/', JSON.stringify(body)) + .matchHeader('Content-Type', 'application/x-www-form-urlencoded') + .post('/', qs.stringify(body)) .reply(200, {}); const res = await request({ url, @@ -530,4 +530,37 @@ describe('🍂 defaults & instances', () => { scope.done(); assert.deepStrictEqual(res.data, body); }); + + it('should allow buffer to be posted', async () => { + const pkg = fs.readFileSync('./package.json'); + const pkgJson = JSON.parse(pkg.toString('utf8')); + const scope = nock(url) + .matchHeader('content-type', 'application/dicom') + .post('/', pkgJson) + .reply(200, {}); + const res = await request({ + url, + method: 'POST', + data: pkg, + headers: {'content-type': 'application/dicom'}, + }); + scope.done(); + assert.deepStrictEqual(res.data, {}); + }); + + it('should set content-type to application/json by default, for buffer', async () => { + const pkg = fs.readFileSync('./package.json'); + const pkgJson = JSON.parse(pkg.toString('utf8')); + const scope = nock(url) + .matchHeader('content-type', 'application/json') + .post('/', pkgJson) + .reply(200, {}); + const res = await request({ + url, + method: 'POST', + data: pkg, + }); + scope.done(); + assert.deepStrictEqual(res.data, {}); + }); }); From 10369f40b5fb4a5257b7c4d3ef0dd8d7d1903f41 Mon Sep 17 00:00:00 2001 From: bcoe Date: Fri, 26 Feb 2021 14:58:31 -0800 Subject: [PATCH 2/3] chore: address code review --- src/gaxios.ts | 21 +++++++++++++-------- test/test.getch.ts | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/gaxios.ts b/src/gaxios.ts index 053129e4..bfeab361 100644 --- a/src/gaxios.ts +++ b/src/gaxios.ts @@ -44,13 +44,18 @@ function hasBuffer() { return typeof Buffer !== 'undefined'; } -function hasHeader(options: GaxiosOptions, headerCheck: string) { - headerCheck = headerCheck.toLowerCase(); - for (let header of Object.keys(options?.headers || {})) { - header = header.toLowerCase(); - if (header === headerCheck) return true; +function hasHeader(options: GaxiosOptions, header: string) { + return !!getHeader(options, header); +} + +function getHeader(options: GaxiosOptions, header: string): string | undefined { + header = header.toLowerCase(); + for (const key of Object.keys(options?.headers || {})) { + if (header === key.toLowerCase()) { + return options.headers![key]; + } } - return false; + return undefined; } let HttpsProxyAgent: any; @@ -242,8 +247,8 @@ export class Gaxios { // If www-form-urlencoded content type has been set, but data is // provided as an object, serialize the content using querystring: if ( - opts.headers['Content-Type'] && - opts.headers['Content-Type'] === 'application/x-www-form-urlencoded' + getHeader(opts, 'content-type') === + 'application/x-www-form-urlencoded' ) { opts.body = opts.paramsSerializer(opts.data); } else { diff --git a/test/test.getch.ts b/test/test.getch.ts index f47cdfa6..d24fb388 100644 --- a/test/test.getch.ts +++ b/test/test.getch.ts @@ -449,7 +449,7 @@ describe('🎏 data handling', () => { assert.deepStrictEqual(res.data, {}); }); - it('application/x-www-form-urlencoded with object data should stringify with qs', async () => { + it('it should stringify with qs when content-type is set to application/x-www-form-urlencoded', async () => { const body = {hello: '🌎'}; const scope = nock(url) .matchHeader('Content-Type', 'application/x-www-form-urlencoded') From f12389905aaab17bb6c295f6be75313b3f237e04 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Fri, 26 Feb 2021 15:00:14 -0800 Subject: [PATCH 3/3] Update test/test.getch.ts --- test/test.getch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.getch.ts b/test/test.getch.ts index d24fb388..7e542671 100644 --- a/test/test.getch.ts +++ b/test/test.getch.ts @@ -449,7 +449,7 @@ describe('🎏 data handling', () => { assert.deepStrictEqual(res.data, {}); }); - it('it should stringify with qs when content-type is set to application/x-www-form-urlencoded', async () => { + it('should stringify with qs when content-type is set to application/x-www-form-urlencoded', async () => { const body = {hello: '🌎'}; const scope = nock(url) .matchHeader('Content-Type', 'application/x-www-form-urlencoded')