diff --git a/src/gaxios.ts b/src/gaxios.ts index de13c585..bfeab361 100644 --- a/src/gaxios.ts +++ b/src/gaxios.ts @@ -40,6 +40,24 @@ function hasFetch() { return hasWindow() && !!window.fetch; } +function hasBuffer() { + return typeof Buffer !== 'undefined'; +} + +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 undefined; +} + let HttpsProxyAgent: any; function loadProxy() { @@ -219,21 +237,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') + getHeader(opts, '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..7e542671 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('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/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, {}); + }); });