diff --git a/source/create.js b/source/create.js index 55e173a85..3140cc5a3 100644 --- a/source/create.js +++ b/source/create.js @@ -1,11 +1,12 @@ 'use strict'; const URLGlobal = typeof URL === 'undefined' ? require('url').URL : URL; // TODO: Use the `URL` global when targeting Node.js 10 +const extend = require('extend'); const errors = require('./errors'); const assignOptions = require('./assign-options'); const asStream = require('./as-stream'); const asPromise = require('./as-promise'); const normalizeArguments = require('./normalize-arguments'); -const deepFreeze = require('./deep-freeze'); +const defineConstProperty = require('./define-const-property'); const makeNext = defaults => (path, options) => { let url = path; @@ -24,6 +25,8 @@ const makeNext = defaults => (path, options) => { }; const create = defaults => { + defaults = extend(true, {}, defaults); + const next = makeNext(defaults); if (!defaults.handler) { defaults.handler = next; @@ -51,18 +54,19 @@ const create = defaults => { return defaults.handler(url, options, next); }; + got.hooks = defaults.options.hooks = { // eslint-disable-line no-multi-assign + beforeRequest: [], + onSocketConnect: [], + onAbort: [], + ...(defaults.options.hooks || {}) + }; + for (const method of defaults.methods) { got[method] = (url, options) => got(url, {...options, method}); got.stream[method] = (url, options) => got.stream(url, {...options, method}); } - Object.assign(got, errors); - Object.defineProperty(got, 'defaults', { - value: deepFreeze(defaults), - writable: false, - enumerable: true, - configurable: true - }); + defineConstProperty(got, {...errors, defaults}, true, ['defaults.options.hooks']); return got; }; diff --git a/source/deep-freeze.js b/source/deep-freeze.js deleted file mode 100644 index 1d3d8f028..000000000 --- a/source/deep-freeze.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; -const is = require('@sindresorhus/is'); - -module.exports = function deepFreeze(obj) { - for (const [key, value] of Object.entries(obj)) { - if (is.object(value)) { - deepFreeze(obj[key]); - } - } - - return Object.freeze(obj); -}; diff --git a/source/define-const-property.js b/source/define-const-property.js new file mode 100644 index 000000000..8b549a4c6 --- /dev/null +++ b/source/define-const-property.js @@ -0,0 +1,25 @@ +'use strict'; +const is = require('@sindresorhus/is'); + +module.exports = (where, properties, deep, excluded) => { + const deepFreeze = (obj, parent) => { + for (const [key, value] of Object.entries(obj)) { + const name = parent + '.' + key; + + if (is.object(value) && !excluded.includes(name)) { + deepFreeze(obj[key], name); + } + } + + return excluded.includes(parent) ? obj : Object.freeze(obj); + }; + + for (const [key, value] of Object.entries(properties)) { + Object.defineProperty(where, key, { + value: deep ? deepFreeze(value, key) : Object.freeze(value), + writable: false, + enumerable: true, + configurable: true + }); + } +}; diff --git a/source/index.js b/source/index.js index f4573f845..1f9f4c4ba 100644 --- a/source/index.js +++ b/source/index.js @@ -23,9 +23,6 @@ const defaults = { throwHttpErrors: true, headers: { 'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)` - }, - hooks: { - beforeRequest: [] } } }; diff --git a/source/normalize-arguments.js b/source/normalize-arguments.js index e09e97d1c..7082805b9 100644 --- a/source/normalize-arguments.js +++ b/source/normalize-arguments.js @@ -8,7 +8,6 @@ const urlToOptions = require('./url-to-options'); const isFormData = require('./is-form-data'); const retryAfterStatusCodes = new Set([413, 429, 503]); -const knownHookEvents = ['beforeRequest']; module.exports = (url, options, defaults) => { if (Reflect.has(options, 'url') || (is.object(url) && Reflect.has(url, 'url'))) { @@ -187,24 +186,14 @@ module.exports = (url, options, defaults) => { delete options.timeout; } - if (is.nullOrUndefined(options.hooks)) { - options.hooks = {}; - } if (is.object(options.hooks)) { - for (const hookEvent of knownHookEvents) { - const hooks = options.hooks[hookEvent]; - if (is.nullOrUndefined(hooks)) { - options.hooks[hookEvent] = []; - } else if (is.array(hooks)) { - hooks.forEach( - (hook, index) => { - if (!is.function_(hook)) { - throw new TypeError( - `Parameter \`hooks.${hookEvent}[${index}]\` must be a function, not ${is(hook)}` - ); - } + for (const [hookEvent, hooks] of Object.entries(options.hooks)) { + if (is.array(hooks)) { + for (const [index, hook] of Object.entries(hooks)) { + if (!is.function(hook)) { + throw new TypeError(`Parameter \`hooks.${hookEvent}[${index}]\` must be a function, not ${is(hook)}`); } - ); + } } else { throw new TypeError(`Parameter \`hooks.${hookEvent}\` must be an array, not ${is(hooks)}`); } diff --git a/source/request-as-event-emitter.js b/source/request-as-event-emitter.js index 80a9ee6f4..1651826cf 100644 --- a/source/request-as-event-emitter.js +++ b/source/request-as-event-emitter.js @@ -14,6 +14,12 @@ const {CacheError, UnsupportedProtocolError, MaxRedirectsError, RequestError} = const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]); const allMethodRedirectCodes = new Set([300, 303, 307, 308]); +const callAll = async (array, ...args) => { + for (const func of array) { + await func(...args); // eslint-disable-line no-await-in-loop + } +}; + module.exports = (options = {}) => { const emitter = new EventEmitter(); const requestUrl = options.href || (new URLGlobal(options.path, urlLib.format(options))).toString(); @@ -122,8 +128,9 @@ module.exports = (options = {}) => { cacheReq.once('request', req => { let aborted = false; - req.once('abort', _ => { + req.once('abort', () => { aborted = true; + callAll(options.hooks.onAbort); }); req.once('error', error => { @@ -150,7 +157,9 @@ module.exports = (options = {}) => { const socket = req.connection; if (socket) { - const onSocketConnect = () => { + const onSocketConnect = async () => { + await callAll(options.hooks.onSocketConnect); + const uploadEventFrequency = 150; progressInterval = setInterval(() => { @@ -241,10 +250,7 @@ module.exports = (options = {}) => { options.headers['content-length'] = uploadBodySize; } - for (const hook of options.hooks.beforeRequest) { - // eslint-disable-next-line no-await-in-loop - await hook(options); - } + await callAll(options.hooks.beforeRequest, options); get(options); } catch (error) { diff --git a/test/arguments.js b/test/arguments.js index ab70ab276..6b868af4c 100644 --- a/test/arguments.js +++ b/test/arguments.js @@ -132,7 +132,7 @@ test('throws TypeError when known `hooks` array item is not a function', async t }); test('allows extra keys in `hooks`', async t => { - await t.notThrows(() => got(`${s.url}/test`, {hooks: {extra: {}}})); + await t.notThrows(() => got(`${s.url}/test`, {hooks: {extra: []}})); }); test.after('cleanup', async () => { diff --git a/test/hooks.js b/test/hooks.js index 220bf97ce..98045a2e7 100644 --- a/test/hooks.js +++ b/test/hooks.js @@ -7,97 +7,92 @@ let s; test.before('setup', async () => { s = await createServer(); - const echoHeaders = (req, res) => { + s.on('/', async (req, res) => { + await delay(500); res.statusCode = 200; - res.write(JSON.stringify(req.headers)); - res.end(); - }; - s.on('/', echoHeaders); + res.end(JSON.stringify(req.headers)); + }); await s.listen(s.port); }); test('beforeRequest receives normalized options', async t => { - await got( - s.url, - { - json: true, - hooks: { - beforeRequest: [ - options => { - t.is(options.path, '/'); - t.is(options.hostname, 'localhost'); - } - ] - } + await got(s.url, { + json: true, + hooks: { + beforeRequest: [ + options => { + t.is(options.path, '/'); + t.is(options.hostname, 'localhost'); + } + ] } - ); + }); }); test('beforeRequest allows modifications', async t => { - const res = await got( - s.url, - { - json: true, - hooks: { - beforeRequest: [ - options => { - options.headers.foo = 'bar'; - } - ] - } + const res = await got(s.url, { + json: true, + hooks: { + beforeRequest: [ + options => { + options.headers.foo = 'bar'; + } + ] } - ); + }); t.is(res.body.foo, 'bar'); }); test('beforeRequest awaits async function', async t => { - const res = await got( - s.url, - { - json: true, - hooks: { - beforeRequest: [ - async options => { - await delay(100); - options.headers.foo = 'bar'; - } - ] - } + const res = await got(s.url, { + json: true, + hooks: { + beforeRequest: [ + async options => { + await delay(100); + options.headers.foo = 'bar'; + } + ] } - ); + }); t.is(res.body.foo, 'bar'); }); test('beforeRequest rejects when beforeRequest throws', async t => { - await t.throws( - () => got(s.url, { - hooks: { - beforeRequest: [ - () => { - throw new Error('oops'); - } - ] - } - }), - { - instanceOf: Error, - message: 'oops' + await t.throws(got(s.url, { + hooks: { + beforeRequest: [ + () => { + throw new Error('oops'); + } + ] } - ); + }), {message: 'oops'}); }); test('beforeRequest rejects when beforeRequest rejects', async t => { - await t.throws( - () => got(s.url, { - hooks: { - beforeRequest: [() => Promise.reject(new Error('oops'))] - } - }), - { - instanceOf: Error, - message: 'oops' + await t.throws(got(s.url, { + hooks: { + beforeRequest: [() => Promise.reject(new Error('oops'))] } - ); + }), {message: 'oops'}); +}); + +test('extend got + onAbort hook', async t => { + let aborted = false; + + const extended = got.extend(); + extended.hooks.onAbort.push(() => { + aborted = true; + }); + + const p = extended(s.url); + p.cancel(); + + await t.throws(p); + await delay(200); // Wait because it may throw before the hook is called + + t.is(aborted, true); }); test.after('cleanup', async () => {