UNPKG

gaxios

Version:

A simple common HTTP client specifically for Google APIs and services.

1,065 lines 64.4 kB
"use strict"; // Copyright 2018 Google LLC // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const assert_1 = __importDefault(require("assert")); const nock_1 = __importDefault(require("nock")); const sinon_1 = __importDefault(require("sinon")); const stream_1 = __importStar(require("stream")); const mocha_1 = require("mocha"); const https_proxy_agent_1 = require("https-proxy-agent"); const index_js_1 = require("../src/index.js"); const common_js_1 = require("../src/common.js"); const util_cjs_1 = __importDefault(require("../src/util.cjs")); const fs_1 = __importDefault(require("fs")); const pkg = util_cjs_1.default.pkg; nock_1.default.disableNetConnect(); const sandbox = sinon_1.default.createSandbox(); (0, mocha_1.afterEach)(() => { sandbox.restore(); nock_1.default.cleanAll(); }); const url = 'https://example.com'; function setEnv(obj) { return sandbox.stub(process, 'env').value(obj); } (0, mocha_1.describe)('🦖 option validation', () => { (0, mocha_1.it)('should throw an error if a url is not provided', async () => { await assert_1.default.rejects((0, index_js_1.request)({}), /URL is required/); }); }); (0, mocha_1.describe)('🚙 error handling', () => { (0, mocha_1.it)('should throw on non-2xx responses by default', async () => { const scope = (0, nock_1.default)(url).get('/').reply(500); await assert_1.default.rejects((0, index_js_1.request)({ url }), (err) => { scope.done(); return err.status === 500; }); }); (0, mocha_1.it)('should throw the error as a GaxiosError object, regardless of Content-Type header', async () => { const body = { error: { message: 'File not found', code: 404, status: 'NOT FOUND', details: [ { some: 'details', }, ], }, }; const scope = (0, nock_1.default)(url).get('/').reply(404, body); await assert_1.default.rejects((0, index_js_1.request)({ url, responseType: 'json' }), (err) => { scope.done(); assert_1.default.deepStrictEqual(err.cause, body.error); return err.status === 404 && err.message === 'File not found'; }); }); (0, mocha_1.it)('should throw the error as a `GaxiosError` object (with the message as a string), even if the request type is requested as an arraybuffer', async () => { const body = { error: { status: 404, message: 'File not found', }, }; const scope = (0, nock_1.default)(url).get('/').reply(404, body); await assert_1.default.rejects((0, index_js_1.request)({ url, responseType: 'arraybuffer' }), (err) => { scope.done(); return (err.status === 404 && err.message === 'Request failed with status code 404' && err.response?.data.error.message === 'File not found'); }); }); (0, mocha_1.it)('should not throw an error during a translation error', () => { const notJSON = '.'; const response = { config: { responseType: 'json', }, data: notJSON, status: 500, statusText: '', headers: new Headers(), // workaround for `node-fetch`'s `.data` deprecation... bodyUsed: true, }; const error = new index_js_1.GaxiosError('translation test', {}, response); (0, assert_1.default)(error.response); assert_1.default.equal(error.response.data, notJSON); }); (0, mocha_1.it)('should support `instanceof` for GaxiosErrors of the same version', () => { class A extends index_js_1.GaxiosError { } const wrongVersion = { [common_js_1.GAXIOS_ERROR_SYMBOL]: '0.0.0' }; const correctVersion = { [common_js_1.GAXIOS_ERROR_SYMBOL]: pkg.version }; const child = new A('', {}); assert_1.default.equal(wrongVersion instanceof index_js_1.GaxiosError, false); assert_1.default.equal(correctVersion instanceof index_js_1.GaxiosError, true); assert_1.default.equal(child instanceof index_js_1.GaxiosError, true); }); }); (0, mocha_1.describe)('🥁 configuration options', () => { (0, mocha_1.it)('should accept `URL` objects', async () => { const scope = (0, nock_1.default)(url).get('/').reply(204); const res = await (0, index_js_1.request)({ url: new URL(url) }); scope.done(); assert_1.default.strictEqual(res.status, 204); }); (0, mocha_1.it)('should accept `Request` objects', async () => { const scope = (0, nock_1.default)(url).get('/').reply(204); const res = await (0, index_js_1.request)(new Request(url)); scope.done(); assert_1.default.strictEqual(res.status, 204); }); (0, mocha_1.it)('should use options passed into the constructor', async () => { const scope = (0, nock_1.default)(url).head('/').reply(200); const inst = new index_js_1.Gaxios({ method: 'HEAD' }); const res = await inst.request({ url }); scope.done(); assert_1.default.strictEqual(res.config.method, 'HEAD'); }); (0, mocha_1.it)('should handle nested options passed into the constructor', async () => { const scope = (0, nock_1.default)(url).get('/').reply(200); const inst = new index_js_1.Gaxios({ headers: new Headers({ apple: 'juice' }) }); const res = await inst.request({ url, headers: { figgy: 'pudding' }, }); scope.done(); assert_1.default.strictEqual(res.config.headers.get('apple'), 'juice'); assert_1.default.strictEqual(res.config.headers.get('figgy'), 'pudding'); }); (0, mocha_1.it)('should allow setting a base url in the options', async () => { const scope = (0, nock_1.default)(url).get('/v1/mango').reply(200, {}); const inst = new index_js_1.Gaxios({ baseURL: `${url}/v1/` }); const res = await inst.request({ url: 'mango' }); scope.done(); assert_1.default.deepStrictEqual(res.data, {}); }); (0, mocha_1.it)('should allow overriding valid status', async () => { const scope = (0, nock_1.default)(url).get('/').reply(304); const res = await (0, index_js_1.request)({ url, validateStatus: () => true }); scope.done(); assert_1.default.strictEqual(res.status, 304); }); (0, mocha_1.it)('should allow setting maxContentLength', async () => { const body = { hello: '🌎' }; const scope = (0, nock_1.default)(url) .get('/') .reply(200, body, { 'content-length': body.toString().length.toString() }); const maxContentLength = 1; await assert_1.default.rejects((0, index_js_1.request)({ url, maxContentLength }), (err) => { return err instanceof index_js_1.GaxiosError && /limit/.test(err.message); }); scope.done(); }); (0, mocha_1.it)('should support redirects by default', async () => { const body = { hello: '🌎' }; const scopes = [ (0, nock_1.default)(url).get('/foo').reply(200, body), (0, nock_1.default)(url).get('/').reply(302, undefined, { location: '/foo' }), ]; const res = await (0, index_js_1.request)({ url }); scopes.forEach(x => x.done()); assert_1.default.deepStrictEqual(res.data, body); assert_1.default.strictEqual(res.url, `${url}/foo`); }); (0, mocha_1.it)('should allow overriding the adapter', async () => { const response = { data: { hello: '🌎' }, config: {}, status: 200, statusText: 'OK', headers: new Headers(), }; const adapter = () => Promise.resolve(response); const res = await (0, index_js_1.request)({ url, adapter }); assert_1.default.strictEqual(response, res); }); (0, mocha_1.it)('should allow overriding the adapter with default adapter wrapper', async () => { const body = { hello: '🌎' }; const extraProperty = '🦦'; const scope = (0, nock_1.default)(url).get('/').reply(200, body); const timings = []; const res = await (0, index_js_1.request)({ url, adapter: async (opts, defaultAdapter) => { const begin = Date.now(); const res = await defaultAdapter(opts); const end = Date.now(); res.data = { ...res.data, extraProperty, }; timings.push({ duration: end - begin }); return res; }, }); scope.done(); assert_1.default.deepStrictEqual(res.data, { ...body, extraProperty, }); (0, assert_1.default)(timings.length === 1); (0, assert_1.default)(typeof timings[0].duration === 'number'); }); (0, mocha_1.it)('should encode URL parameters', async () => { const path = '/?james=kirk&montgomery=scott'; const opts = { url: `${url}${path}` }; const scope = (0, nock_1.default)(url).get(path).reply(200, {}); const res = await (0, index_js_1.request)(opts); assert_1.default.strictEqual(res.status, 200); assert_1.default.strictEqual(res.config.url?.toString(), url + path); scope.done(); }); (0, mocha_1.it)('should preserve the original querystring', async () => { const path = '/?robot'; const opts = { url: `${url}${path}` }; const scope = (0, nock_1.default)(url).get(path).reply(200, {}); const res = await (0, index_js_1.request)(opts); assert_1.default.strictEqual(res.status, 200); assert_1.default.strictEqual(res.config.url?.toString(), url + path); scope.done(); }); (0, mocha_1.it)('should handle empty querystring params', async () => { const scope = (0, nock_1.default)(url).get('/').reply(200, {}); const res = await (0, index_js_1.request)({ url, params: {}, }); assert_1.default.strictEqual(res.status, 200); scope.done(); }); (0, mocha_1.it)('should encode parameters from the params option', async () => { const opts = { url, params: { james: 'kirk', montgomery: 'scott' } }; const qs = '?james=kirk&montgomery=scott'; const path = `/${qs}`; const scope = (0, nock_1.default)(url).get(path).reply(200, {}); const res = await (0, index_js_1.request)(opts); assert_1.default.strictEqual(res.status, 200); assert_1.default.strictEqual(res.config.url?.toString(), new URL(url + qs).toString()); scope.done(); }); (0, mocha_1.it)('should merge URL parameters with the params option', async () => { const opts = { url: `${url}/?james=beckwith&montgomery=scott`, params: { james: 'kirk' }, }; const path = '/?james=beckwith&montgomery=scott&james=kirk'; const scope = (0, nock_1.default)(url).get(path).reply(200, {}); const res = await (0, index_js_1.request)(opts); assert_1.default.strictEqual(res.status, 200); assert_1.default.strictEqual(res.config.url?.toString(), url + path); scope.done(); }); (0, mocha_1.it)('should allow overriding the param serializer', async () => { const qs = '?oh=HAI'; const params = { james: 'kirk' }; const opts = { url, params, paramsSerializer: ps => { assert_1.default.strictEqual(JSON.stringify(params), JSON.stringify(ps)); return '?oh=HAI'; }, }; const scope = (0, nock_1.default)(url).get(`/${qs}`).reply(200, {}); const res = await (0, index_js_1.request)(opts); assert_1.default.strictEqual(res.status, 200); assert_1.default.strictEqual(res.config.url.toString(), new URL(url + qs).toString()); scope.done(); }); (0, mocha_1.it)('should return json by default', async () => { const body = { hello: '🌎' }; const scope = (0, nock_1.default)(url).get('/').reply(200, body); const res = await (0, index_js_1.request)({ url }); scope.done(); assert_1.default.deepStrictEqual(body, res.data); }); (0, mocha_1.it)('should send an application/json header by default', async () => { const scope = (0, nock_1.default)(url) .matchHeader('accept', 'application/json') .get('/') .reply(200, {}); const res = await (0, index_js_1.request)({ url, responseType: 'json' }); scope.done(); assert_1.default.deepStrictEqual(res.data, {}); }); (0, mocha_1.describe)('proxying', () => { const url = 'https://domain.example.com/with-path'; const proxy = 'https://fake.proxy/'; let gaxios; let request; let responseBody; let scope; beforeEach(() => { gaxios = new index_js_1.Gaxios(); request = gaxios.request.bind(gaxios); responseBody = { hello: '🌎' }; const direct = new URL(url); scope = (0, nock_1.default)(direct.origin).get(direct.pathname).reply(200, responseBody); }); function expectDirect(res) { scope.done(); assert_1.default.deepStrictEqual(res.data, responseBody); assert_1.default.strictEqual(res.config.agent, undefined); } function expectProxy(res) { scope.done(); assert_1.default.deepStrictEqual(res.data, responseBody); assert_1.default.ok(res.config.agent instanceof https_proxy_agent_1.HttpsProxyAgent); assert_1.default.equal(res.config.agent.proxy.toString(), proxy); } (0, mocha_1.it)('should use an https proxy if asked nicely (config)', async () => { const res = await request({ url, proxy }); expectProxy(res); }); (0, mocha_1.it)('should use an https proxy if asked nicely (env)', async () => { setEnv({ https_proxy: proxy }); const res = await request({ url }); expectProxy(res); }); (0, mocha_1.it)('should use mTLS with proxy', async () => { const cert = 'cert'; const key = 'key'; const res = await request({ url, proxy, cert, key }); expectProxy(res); (0, assert_1.default)(res.config.agent instanceof https_proxy_agent_1.HttpsProxyAgent); assert_1.default.equal(res.config.agent.connectOpts.cert, cert); assert_1.default.equal(res.config.agent.connectOpts.key, key); }); (0, mocha_1.it)('should load the proxy from the cache', async () => { const res1 = await request({ url, proxy }); const agent = res1.config.agent; expectProxy(res1); const direct = new URL(url); scope = (0, nock_1.default)(direct.origin).get(direct.pathname).reply(200, responseBody); const res2 = await request({ url, proxy }); assert_1.default.strictEqual(agent, res2.config.agent); expectProxy(res2); }); (0, mocha_1.it)('should load the proxy from the cache with mTLS', async () => { const cert = 'cert'; const key = 'key'; const res1 = await request({ url, proxy, cert, key }); const agent = res1.config.agent; expectProxy(res1); const direct = new URL(url); scope = (0, nock_1.default)(direct.origin).get(direct.pathname).reply(200, responseBody); const res2 = await request({ url, proxy }); assert_1.default.strictEqual(agent, res2.config.agent); expectProxy(res2); (0, assert_1.default)(res2.config.agent instanceof https_proxy_agent_1.HttpsProxyAgent); assert_1.default.equal(res2.config.agent.connectOpts.cert, cert); assert_1.default.equal(res2.config.agent.connectOpts.key, key); }); (0, mocha_1.describe)('noProxy', () => { (0, mocha_1.it)('should not proxy when url matches `noProxy` (config > string)', async () => { const noProxy = [new URL(url).host]; const res = await request({ url, proxy, noProxy }); expectDirect(res); }); (0, mocha_1.it)('should not proxy when url matches `noProxy` (config > URL)', async () => { // should match by `URL#origin` const noProxyURL = new URL(url); noProxyURL.pathname = '/some-other-path'; const noProxy = [noProxyURL]; const res = await request({ url, proxy, noProxy }); expectDirect(res); }); (0, mocha_1.it)('should not proxy when url matches `noProxy` (config > RegExp)', async () => { const noProxy = [/example.com/]; const res = await request({ url, proxy, noProxy }); expectDirect(res); }); (0, mocha_1.it)('should not proxy when url matches `noProxy` (config + env > match config)', async () => { const noProxy = [url]; setEnv({ no_proxy: 'https://foo.bar' }); const res = await request({ url, proxy, noProxy }); expectDirect(res); }); (0, mocha_1.it)('should not proxy when url matches `noProxy` (config + env > match env)', async () => { const noProxy = ['https://foo.bar']; setEnv({ no_proxy: url }); const res = await request({ url, proxy, noProxy }); expectDirect(res); }); (0, mocha_1.it)('should proxy when url does not match `noProxy` (config > string)', async () => { const noProxy = [url]; const res = await request({ url, proxy, noProxy }); expectDirect(res); }); (0, mocha_1.it)('should proxy if url does not match `noProxy` (config > URL > diff origin > protocol)', async () => { const noProxyURL = new URL(url); noProxyURL.protocol = 'http:'; const noProxy = [noProxyURL]; const res = await request({ url, proxy, noProxy }); expectProxy(res); }); (0, mocha_1.it)('should proxy if url does not match `noProxy` (config > URL > diff origin > port)', async () => { const noProxyURL = new URL(url); noProxyURL.port = '8443'; const noProxy = [noProxyURL]; const res = await request({ url, proxy, noProxy }); expectProxy(res); }); (0, mocha_1.it)('should proxy if url does not match `noProxy` (env)', async () => { setEnv({ https_proxy: proxy, no_proxy: 'https://blah' }); const res = await request({ url }); expectProxy(res); }); (0, mocha_1.it)('should not proxy if `noProxy` env var matches the origin or hostname of the URL (config > string)', async () => { const noProxy = [new URL(url).hostname]; const res = await request({ url, proxy, noProxy }); expectDirect(res); }); (0, mocha_1.it)('should not proxy if `noProxy` env var matches the origin or hostname of the URL (env)', async () => { setEnv({ https_proxy: proxy, no_proxy: new URL(url).hostname }); const res = await request({ url }); expectDirect(res); }); (0, mocha_1.it)('should not proxy if `noProxy` env variable has asterisk, and URL partially matches (config)', async () => { const parentHost = new URL(url).hostname.split('.').slice(1).join('.'); // ensure we have a host for a valid test (0, assert_1.default)(parentHost); const noProxy = [`*.${parentHost}`]; const res = await request({ url, proxy, noProxy }); expectDirect(res); }); (0, mocha_1.it)('should not proxy if `noProxy` env variable has asterisk, and URL partially matches (env)', async () => { const parentHost = new URL(url).hostname.split('.').slice(1).join('.'); // ensure we have a host for a valid test (0, assert_1.default)(parentHost); setEnv({ https_proxy: proxy, no_proxy: `*.${parentHost}` }); const res = await request({ url }); expectDirect(res); }); (0, mocha_1.it)('should not proxy if `noProxy` env variable starts with a dot, and URL partially matches (config)', async () => { const parentHost = new URL(url).hostname.split('.').slice(1).join('.'); // ensure we have a host for a valid test (0, assert_1.default)(parentHost); const noProxy = [`.${parentHost}`]; const res = await request({ url, proxy, noProxy }); expectDirect(res); }); (0, mocha_1.it)('should not proxy if `noProxy` env variable starts with a dot, and URL partially matches (env)', async () => { const parentHost = new URL(url).hostname.split('.').slice(1).join('.'); // ensure we have a host for a valid test (0, assert_1.default)(parentHost); setEnv({ https_proxy: proxy, no_proxy: '.example.com' }); const res = await request({ url }); expectDirect(res); }); (0, mocha_1.it)('should proxy if `noProxy` env variable has asterisk, but URL is not matching (config)', async () => { const noProxy = ['*.no.match']; const res = await request({ url, proxy, noProxy }); expectProxy(res); }); (0, mocha_1.it)('should proxy if `noProxy` env variable has asterisk, but URL is not matching (env)', async () => { setEnv({ https_proxy: proxy, no_proxy: '*.no.match' }); const res = await request({ url }); expectProxy(res); }); (0, mocha_1.it)('should allow comma-separated lists for `noProxy` env variables (config)', async () => { const parentHost = new URL(url).hostname.split('.').slice(1).join('.'); // ensure we have a host for a valid test (0, assert_1.default)(parentHost); const noProxy = ['google.com', `*.${parentHost}`, 'hello.com']; const res = await request({ url, proxy, noProxy }); expectDirect(res); }); (0, mocha_1.it)('should allow comma-separated lists for `noProxy` env variables (env)', async () => { const parentHost = new URL(url).hostname.split('.').slice(1).join('.'); // ensure we have a host for a valid test (0, assert_1.default)(parentHost); // added spaces to ensure trimming works as expected const noProxy = [' google.com ', ` *.${parentHost} `, ' hello.com ']; setEnv({ https_proxy: proxy, no_proxy: noProxy.join(',') }); const res = await request({ url }); expectDirect(res); }); }); }); (0, mocha_1.it)('should include the request data in the response config', async () => { const body = { hello: '🌎' }; const scope = (0, nock_1.default)(url).post('/', body).reply(200); const res = await (0, index_js_1.request)({ url, method: 'POST', data: body }); scope.done(); assert_1.default.deepStrictEqual(res.config.data, body); }); (0, mocha_1.it)('should not stringify the data if it is appended by a form', async () => { const formData = new FormData(); formData.append('test', '123'); const scope = (0, nock_1.default)(url) .post('/', body => { /** * Sample from native `node-fetch` * body: '------3785545705014550845559551617\r\n' + * 'Content-Disposition: form-data; name="test"\r\n' + * '\r\n' + * '123\r\n' + * '------3785545705014550845559551617--', */ /** * Sample from native `fetch` * body: '------formdata-undici-0.39470493152687736\r\n' + * 'Content-Disposition: form-data; name="test"\r\n' + * '\r\n' + * '123\r\n' + * '------formdata-undici-0.39470493152687736--', */ return body.match('Content-Disposition: form-data;'); }) .reply(200); const res = await (0, index_js_1.request)({ url, method: 'POST', data: formData, }); scope.done(); assert_1.default.deepStrictEqual(res.config.data, formData); assert_1.default.ok(res.config.body instanceof FormData); assert_1.default.ok(res.config.data instanceof FormData); }); (0, mocha_1.it)('should allow explicitly setting the fetch implementation', async () => { let customFetchCalled = false; const myFetch = (...args) => { customFetchCalled = true; return fetch(...args); }; const scope = (0, nock_1.default)(url).post('/').reply(204); const res = await (0, index_js_1.request)({ url, method: 'POST', fetchImplementation: myFetch, // This `data` ensures the 'duplex' option has been set data: { sample: 'data' }, }); (0, assert_1.default)(customFetchCalled); assert_1.default.equal(res.status, 204); scope.done(); }); (0, mocha_1.it)('should be able to disable the `errorRedactor`', async () => { const scope = (0, nock_1.default)(url).get('/').reply(200); const instance = new index_js_1.Gaxios({ url, errorRedactor: false }); assert_1.default.equal(instance.defaults.errorRedactor, false); await instance.request({ url }); scope.done(); assert_1.default.equal(instance.defaults.errorRedactor, false); }); (0, mocha_1.it)('should be able to set a custom `errorRedactor`', async () => { const scope = (0, nock_1.default)(url).get('/').reply(200); const errorRedactor = (t) => t; const instance = new index_js_1.Gaxios({ url, errorRedactor }); assert_1.default.equal(instance.defaults.errorRedactor, errorRedactor); await instance.request({ url }); scope.done(); assert_1.default.equal(instance.defaults.errorRedactor, errorRedactor); }); (0, mocha_1.describe)('timeout', () => { (0, mocha_1.it)('should accept and use a `timeout`', async () => { (0, nock_1.default)(url).get('/').delay(2000).reply(204); const gaxios = new index_js_1.Gaxios(); const timeout = 10; await assert_1.default.rejects(() => gaxios.request({ url, timeout }), /abort/); }); (0, mocha_1.it)('should a `timeout`, an existing `signal`, and be triggered by timeout', async () => { (0, nock_1.default)(url).get('/').delay(2000).reply(204); const gaxios = new index_js_1.Gaxios(); const signal = new AbortController().signal; const timeout = 10; await assert_1.default.rejects(() => gaxios.request({ url, timeout, signal }), /abort/); }); (0, mocha_1.it)('should use a `timeout`, a `signal`, and be triggered by signal', async () => { (0, nock_1.default)(url).get('/').delay(2000).reply(204); const gaxios = new index_js_1.Gaxios(); const ac = new AbortController(); const signal = ac.signal; const timeout = 4000; // after network delay, so this shouldn't trigger const message = 'Changed my mind - no request please'; setTimeout(() => ac.abort(message), 10); await assert_1.default.rejects(() => gaxios.request({ url, timeout, signal }), // `node-fetch` always rejects with the generic 'abort' error: /abort/); }); }); }); (0, mocha_1.describe)('🎏 data handling', () => { (0, mocha_1.it)('should accept a ReadableStream as request data', async () => { const scope = (0, nock_1.default)(url).post('/', 'test').reply(200, {}); const res = await (0, index_js_1.request)({ url, method: 'POST', data: stream_1.Readable.from('test'), }); scope.done(); assert_1.default.deepStrictEqual(res.data, {}); }); (0, mocha_1.it)('should accept a string in the request data', async () => { const body = { hello: '🌎' }; const encoded = new URLSearchParams(body); const scope = (0, nock_1.default)(url) .matchHeader('content-type', 'application/x-www-form-urlencoded') .post('/', encoded.toString()) .reply(200, {}); const res = await (0, index_js_1.request)({ url, method: 'POST', data: encoded, headers: new Headers({ 'content-type': 'application/x-www-form-urlencoded', }), }); scope.done(); assert_1.default.deepStrictEqual(res.data, {}); }); (0, mocha_1.it)('should set application/json content-type for object request by default', async () => { const body = { hello: '🌎' }; const scope = (0, nock_1.default)(url) .matchHeader('Content-Type', 'application/json') .post('/', JSON.stringify(body)) .reply(200, {}); const res = await (0, index_js_1.request)({ url, method: 'POST', data: body, }); scope.done(); assert_1.default.deepStrictEqual(res.data, {}); }); (0, mocha_1.it)('should allow other JSON content-types to be specified', async () => { const body = { hello: '🌎' }; const scope = (0, nock_1.default)(url) .matchHeader('Content-Type', 'application/json-patch+json') .post('/', JSON.stringify(body)) .reply(200, {}); const res = await (0, index_js_1.request)({ url, method: 'POST', data: body, headers: new Headers({ 'Content-Type': 'application/json-patch+json', }), }); scope.done(); assert_1.default.deepStrictEqual(res.data, {}); }); (0, mocha_1.it)('should stringify with qs when content-type is set to application/x-www-form-urlencoded', async () => { const body = { hello: '🌎' }; const scope = (0, nock_1.default)(url) .matchHeader('Content-Type', 'application/x-www-form-urlencoded') .post('/', new URLSearchParams(body).toString()) .reply(200, {}); const res = await (0, index_js_1.request)({ url, method: 'POST', data: body, headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded', }), }); scope.done(); assert_1.default.deepStrictEqual(res.data, {}); }); (0, mocha_1.it)('should return stream if asked nicely', async () => { const body = { hello: '🌎' }; const scope = (0, nock_1.default)(url).get('/').reply(200, body); const res = await (0, index_js_1.request)({ url, responseType: 'stream' }); scope.done(); (0, assert_1.default)(res.data instanceof stream_1.default.Readable); }); (0, mocha_1.it)('should return a `ReadableStream` when `fetch` has been provided ', async () => { const body = { hello: '🌎' }; const scope = (0, nock_1.default)(url).get('/').reply(200, body); const res = await (0, index_js_1.request)({ url, responseType: 'stream', fetchImplementation: fetch, }); scope.done(); (0, assert_1.default)(res.data instanceof ReadableStream); }); (0, mocha_1.it)('should return an ArrayBuffer if asked nicely', async () => { const body = { hello: '🌎' }; const scope = (0, nock_1.default)(url).get('/').reply(200, body); const res = await (0, index_js_1.request)({ url, responseType: 'arraybuffer', }); scope.done(); (0, assert_1.default)(res.data instanceof ArrayBuffer); assert_1.default.deepStrictEqual(Buffer.from(JSON.stringify(body)), Buffer.from(res.data)); }); (0, mocha_1.it)('should return a blob if asked nicely', async () => { const body = { hello: '🌎' }; const scope = (0, nock_1.default)(url).get('/').reply(200, body); const res = await (0, index_js_1.request)({ url, responseType: 'blob' }); scope.done(); assert_1.default.ok(res.data); }); (0, mocha_1.it)('should return text if asked nicely', async () => { const body = 'hello 🌎'; const scope = (0, nock_1.default)(url).get('/').reply(200, body); const res = await (0, index_js_1.request)({ url, responseType: 'text' }); scope.done(); assert_1.default.strictEqual(res.data, body); }); (0, mocha_1.it)('should return status text', async () => { const body = { hello: '🌎' }; const scope = (0, nock_1.default)(url).get('/').reply(200, body); const res = await (0, index_js_1.request)({ url }); scope.done(); assert_1.default.ok(res.data); // node-fetch and native fetch specs differ... // https://github.com/node-fetch/node-fetch/issues/1066 assert_1.default.strictEqual(typeof res.statusText, 'string'); // assert.strictEqual(res.statusText, 'OK'); }); (0, mocha_1.it)('should return JSON when response Content-Type=application/json', async () => { const body = { hello: 'world' }; const scope = (0, nock_1.default)(url) .get('/') .reply(200, body, { 'Content-Type': 'application/json' }); const res = await (0, index_js_1.request)({ url }); scope.done(); assert_1.default.ok(res.data); assert_1.default.deepStrictEqual(res.data, body); }); (0, mocha_1.it)('should return invalid JSON as text when response Content-Type=application/json', async () => { const body = 'hello world'; const scope = (0, nock_1.default)(url) .get('/') .reply(200, body, { 'Content-Type': 'application/json' }); const res = await (0, index_js_1.request)({ url }); scope.done(); assert_1.default.ok(res.data); assert_1.default.deepStrictEqual(res.data, body); }); (0, mocha_1.it)('should return text when response Content-Type=text/plain', async () => { const body = 'hello world'; const scope = (0, nock_1.default)(url) .get('/') .reply(200, body, { 'Content-Type': 'text/plain' }); const res = await (0, index_js_1.request)({ url }); scope.done(); assert_1.default.ok(res.data); assert_1.default.deepStrictEqual(res.data, body); }); (0, mocha_1.it)('should return text when response Content-Type=text/csv', async () => { const body = '"col1","col2"\n"hello","world"'; const scope = (0, nock_1.default)(url) .get('/') .reply(200, body, { 'Content-Type': 'text/csv' }); const res = await (0, index_js_1.request)({ url }); scope.done(); assert_1.default.ok(res.data); assert_1.default.deepStrictEqual(res.data, body); }); (0, mocha_1.it)('should return raw data when Content-Type is unable to be parsed', async () => { const body = Buffer.from('hello world', 'utf-8'); const scope = (0, nock_1.default)(url) .get('/') .reply(200, body, { 'Content-Type': 'image/gif' }); const res = await (0, index_js_1.request)({ url }); scope.done(); assert_1.default.ok(res.data); assert_1.default.notEqual(res.data, body); }); (0, mocha_1.it)('should handle multipart/related when options.multipart is set and a single part', async () => { const bodyContent = { hello: '🌎' }; const body = new stream_1.Readable(); body.push(JSON.stringify(bodyContent)); body.push(null); const scope = (0, nock_1.default)(url) .matchHeader('Content-Type', /multipart\/related; boundary=[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/) .post('/', /^(--[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[\r\n]+Content-Type: application\/json[\r\n\r\n]+{"hello":"🌎"}[\r\n]+--[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}--)$/) .reply(200, {}); const res = await (0, index_js_1.request)({ url, method: 'POST', multipart: [ { headers: new Headers({ 'Content-Type': 'application/json' }), content: body, }, ], }); scope.done(); assert_1.default.ok(res.data); }); (0, mocha_1.it)('should handle multipart/related when options.multipart is set and a multiple parts', async () => { const jsonContent = { hello: '🌎' }; const textContent = 'hello world'; const body = new stream_1.Readable(); body.push(JSON.stringify(jsonContent)); body.push(null); const scope = (0, nock_1.default)(url) .matchHeader('Content-Type', /multipart\/related; boundary=[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/) .post('/', /^(--[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[\r\n]+Content-Type: application\/json[\r\n\r\n]+{"hello":"🌎"}[\r\n]+--[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[\r\n]+Content-Type: text\/plain[\r\n\r\n]+hello world[\r\n]+--[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}--)$/) .reply(200, {}); const res = await (0, index_js_1.request)({ url, method: 'POST', multipart: [ { headers: new Headers({ 'Content-Type': 'application/json' }), content: body, }, { headers: new Headers({ 'Content-Type': 'text/plain' }), content: textContent, }, ], }); scope.done(); assert_1.default.ok(res.data); }); (0, mocha_1.it)('should redact sensitive props via the `errorRedactor` by default', async () => { const REDACT = '<<REDACTED> - See `errorRedactor` option in `gaxios` for configuration>.'; const customURL = new URL(url); customURL.searchParams.append('token', 'sensitive'); customURL.searchParams.append('client_secret', 'data'); customURL.searchParams.append('random', 'non-sensitive'); const config = { headers: { Authentication: 'My Auth', /** * Ensure casing is properly handled */ AUTHORIZATION: 'My Auth', 'content-type': 'application/x-www-form-urlencoded', random: 'data', }, data: { grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: 'somesensitivedata', unrelated: 'data', client_secret: 'data', }, body: 'grant_type=somesensitivedata&assertion=somesensitivedata&client_secret=data', }; // simulate JSON response const responseHeaders = { ...config.headers, 'content-type': 'application/json', }; const response = { ...config.data }; const scope = (0, nock_1.default)(url) .post('/') .query(() => true) .reply(404, response, responseHeaders); const instance = new index_js_1.Gaxios(JSON.parse(JSON.stringify(config))); const requestConfig = { url: customURL.toString(), method: 'POST', }; const requestConfigCopy = JSON.parse(JSON.stringify({ ...requestConfig })); try { await instance.request(requestConfig); throw new Error('Expected a GaxiosError'); } catch (e) { (0, assert_1.default)(e instanceof index_js_1.GaxiosError); // config should not be mutated assert_1.default.deepStrictEqual(instance.defaults, config); assert_1.default.deepStrictEqual(requestConfig, requestConfigCopy); assert_1.default.notStrictEqual(e.config, config); // config redactions - headers const expectedRequestHeaders = new Headers({ ...config.headers, // non-redactables should be present Authentication: REDACT, AUTHORIZATION: REDACT, }); const actualHeaders = e.config.headers; expectedRequestHeaders.forEach((value, key) => { assert_1.default.equal(actualHeaders.get(key), value); }); // config redactions - data assert_1.default.deepStrictEqual(e.config.data, { ...config.data, // non-redactables should be present grant_type: REDACT, assertion: REDACT, client_secret: REDACT, }); assert_1.default.deepStrictEqual(Object.fromEntries(e.config.body), { ...config.data, // non-redactables should be present grant_type: REDACT, assertion: REDACT, client_secret: REDACT, }); expectedRequestHeaders.forEach((value, key) => { assert_1.default.equal(actualHeaders.get(key), value); }); // config redactions - url (0, assert_1.default)(e.config.url); const resultURL = new URL(e.config.url); assert_1.default.notDeepStrictEqual(resultURL.toString(), customURL.toString()); customURL.searchParams.set('token', REDACT); customURL.searchParams.set('client_secret', REDACT); assert_1.default.deepStrictEqual(resultURL.toString(), customURL.toString()); // response redactions (0, assert_1.default)(e.response); assert_1.default.deepStrictEqual(e.response.config, e.config); const expectedResponseHeaders = new Headers({ ...responseHeaders, // non-redactables should be present }); expectedResponseHeaders.set('authentication', REDACT); expectedResponseHeaders.set('authorization', REDACT); expectedResponseHeaders.forEach((value, key) => { assert_1.default.equal(e.response?.headers.get(key), value); }); assert_1.default.deepStrictEqual(e.response.data, { ...response, // non-redactables should be present assertion: REDACT, client_secret: REDACT, grant_type: REDACT, }); } finally { scope.done(); } }); (0, mocha_1.it)('should redact after final retry', async () => { const customURL = new URL(url); customURL.searchParams.append('token', 'sensitive'); customURL.searchParams.append('client_secret', 'data'); customURL.searchParams.append('random', 'non-sensitive'); const data = { grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: 'somesensitivedata', unrelated: 'data', client_secret: 'data', }; let retryAttempted = false; const config = { url: customURL, method: 'POST', data: new URLSearchParams(data), retry: true, retryConfig: { httpMethodsToRetry: ['POST'], onRetryAttempt: err => { assert_1.default.deepStrictEqual(err.config.data, new URLSearchParams(data)); retryAttempted = true; }, }, }; const scope = (0, nock_1.default)(url) .post('/', data) .query(() => true) .reply(500) .post('/', data) .query(() => true) .reply(204); const gaxios = new index_js_1.Gaxios(); try { await gaxios.request(config); (0, assert_1.default)(retryAttempted); } finally { scope.done(); } }); }); (0, mocha_1.describe)('🍂 defaults & instances', () => { (0, mocha_1.it)('should allow creating a new instance', () => { const requestInstance = new index_js_1.Gaxios(); assert_1.default.strictEqual(typeof requestInstance.request, 'function'); }); (0, mocha_1.it)('should allow passing empty options', async () => { const body = { hello: '🌎' }; const scope = (0, nock_1.default)(url).get('/').reply(200, body); const gax = new index_js_1.Gaxios({ url }); const res = await gax.request(); scope.done(); assert_1.default.deepStrictEqual(res.data, body); }); (0, mocha_1.it)('should allow buffer to be posted', async () => { const pkg = fs_1.default.readFileSync('./package.json'); const pkgJson = JSON.parse(pkg.toString('utf8')); const scope = (0, nock_1.default)(url) .matchHeader('content-type', 'application/dicom') .post('/', pkgJson) .reply(200, {}); const res = await (0, index_js_1.request)({ url, method: 'POST', data: pkg, headers: new Headers({ 'content-type': 'application/dicom' }), }); scope.done(); assert_1.default.deepStrictEqual(res.data, {}); }); (0, mocha_1.it)('should not set a default content-type for buffers', async () => { const jsonLike = '{}'; const data = Buffer.from(jsonLike); const scope = (0, nock_1.default)(url) // no content type should be present .matchHeader('content-type', v => v === undefined) .post('/', jsonLike) .reply(204); const res = await (0, index_js_1.request)({ url, method: 'POST', data }); scope.done(); assert_1.default.equal(res.status, 204); }); (0, mocha_1.describe)('mtls', () => { class GaxiosAssertAgentCache extends index_js_1.Gaxios { getAgentCache() { return this.agentCache; } async _request(opts) { (0, assert_1.default)(opts.agent); return super._request(opts); } } (0, mocha_1.it)('uses HTTPS agent if cert and key provided, on first request', async () => { const key = fs_1.default.readFileSync('./test/fixtures/fake.key', 'utf8'); const scope = (0, nock_1.default)(url).get('/').reply(200); cons