Repository: sindresorhus/got
Pull request #698 (2019-01-12 16:34:13)

Make the query values more strict


Patch:
diff --git a/advanced-creation.md b/advanced-creation.md
index 86de39a..a03bce2 100644
--- a/advanced-creation.md
+++ b/advanced-creation.md
@@ -98,6 +98,8 @@ const defaults = {
 			'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`
 		},
 		hooks: {
+			beforeError: [],
+			init: [],
 			beforeRequest: [],
 			beforeRedirect: [],
 			beforeRetry: [],
diff --git a/migration-guides.md b/migration-guides.md
index 0c59f31..c82de73 100644
--- a/migration-guides.md
+++ b/migration-guides.md
@@ -103,31 +103,23 @@ gotInstance(url, options);
 ```js
 const gotInstance = got.extend({
 	hooks: {
-		beforeRequest: [
+		init: [
 			options => {
+				// Save the original option, so we can look at it in the `afterResponse` hook
+				options.originalJson = options.json;
+
 				if (options.json && options.jsonReplacer) {
-					// #1 solution (faster)
-					const newBody = {};
-					for (const [key, value] of Object.entries(options.body)) {
-						let newValue = value;
-						do {
-							newValue = options.jsonReplacer(key, newValue);
-						} while (typeof newValue === 'object');
-
-						newBody[key] = newValue;
-					}
-					options.body = newBody;
-
-					// #2 solution (slower)
-					options.body = JSON.parse(JSON.stringify(options.body, options.jsonReplacer));
+					options.body = JSON.stringify(options.body, options.jsonReplacer);
+					options.json = false; // We've handled that on our own
 				}
 			}
 		],
 		afterResponse: [
 			response => {
 				const options = response.request.gotOptions;
-				if (options.json && options.jsonReviver) {
-					response.body = JSON.stringify(JSON.parse(response.body, options.jsonReviver));
+				if (options.originalJson && options.jsonReviver) {
+					response.body = JSON.parse(response.body, options.jsonReviver);
+					options.json = false; // We've handled that on our own
 				}
 
 				return response;
diff --git a/package.json b/package.json
index bd9b890..f2bef85 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "got",
-	"version": "9.5.0",
+	"version": "9.6.0",
 	"description": "Simplified HTTP requests",
 	"license": "MIT",
 	"repository": "sindresorhus/got",
@@ -47,7 +47,7 @@
 		"url-parse-lax": "^3.0.0"
 	},
 	"devDependencies": {
-		"ava": "^1.0.1",
+		"ava": "^1.1.0",
 		"coveralls": "^3.0.0",
 		"delay": "^4.1.0",
 		"form-data": "^2.3.3",
@@ -61,8 +61,8 @@
 		"slow-stream": "0.0.4",
 		"tempfile": "^2.0.0",
 		"tempy": "^0.2.1",
-		"tough-cookie": "^2.4.3",
-		"xo": "^0.23.0"
+		"tough-cookie": "^3.0.0",
+		"xo": "^0.24.0"
 	},
 	"ava": {
 		"concurrency": 4
diff --git a/readme.md b/readme.md
index ab1ce44..63ad2ed 100644
--- a/readme.md
+++ b/readme.md
@@ -21,6 +21,8 @@ It was created because the popular [`request`](https://github.com/request/reques
 
 Got is for Node.js. For browsers, we recommend [Ky](https://github.com/sindresorhus/ky).
 
+**This readme reflects the next major version that is currently in development. You probably want [the v9 readme](https://www.npmjs.com/package/got).**
+
 
 ## Highlights
 
@@ -35,7 +37,7 @@ Got is for Node.js. For browsers, we recommend [Ky](https://github.com/sindresor
 - [Errors with metadata](#errors)
 - [JSON mode](#json)
 - [WHATWG URL support](#url)
-- [Hooks](https://github.com/sindresorhus/got#hooks)
+- [Hooks](#hooks)
 - [Instances with custom defaults](#instances)
 - [Composable](advanced-creation.md#merging-instances)
 - [Electron support](#useelectronnet)
@@ -351,6 +353,17 @@ Type: `Object<string, Function[]>`
 
 Hooks allow modifications during the request lifecycle. Hook functions may be async and are run serially.
 
+###### hooks.init
+
+Type: `Function[]`<br>
+Default: `[]`
+
+Called with plain [request options](#options), right before their normalization. This is especially useful in conjunction with [`got.extend()`](#instances) and [`got.create()`](advanced-creation.md) when the input needs custom handling.
+
+See the [Request migration guide](migration-guides.md#breaking-changes) for an example.
+
+**Note**: This hook must be synchronous!
+
 ###### hooks.beforeRequest
 
 Type: `Function[]`<br>
@@ -447,6 +460,35 @@ const instance = got.extend({
 });
 ```
 
+###### hooks.beforeError
+
+Type: `Function[]`<br>
+Default: `[]`
+
+Called with an `Error` instance. The error is passed to the hook right before it's thrown. This is especially useful when you want to have more detailed errors.
+
+**Note**: Errors thrown while normalizing input options are thrown directly and not part of this hook.
+
+```js	
+const got = require('got');	
+
+got('api.github.com/some-endpoint', {	
+	hooks: {	
+		onError: [	
+			error => {	
+				const {response} = error;	
+ 				if (response && response.body) {	
+					error.name = 'GitHubError';	
+					error.message = `${response.body.message} (${error.statusCode})`;	
+				}
+
+ 				return error;	
+			}	
+		]	
+	}	
+});	
+```
+
 #### Response
 
 The response object will typically be a [Node.js HTTP response stream](https://nodejs.org/api/http.html#http_class_http_incomingmessage), however, if returned from the cache it will be a [response-like object](https://github.com/lukechilds/responselike) which behaves in the same way.
diff --git a/source/as-promise.js b/source/as-promise.js
index cd11c5a..c502325 100644
--- a/source/as-promise.js
+++ b/source/as-promise.js
@@ -81,6 +81,7 @@ const asPromise = options => {
 
 					resolve(response);
 				}
+
 				return;
 			}
 
diff --git a/source/as-stream.js b/source/as-stream.js
index 09da656..98c5342 100644
--- a/source/as-stream.js
+++ b/source/as-stream.js
@@ -83,6 +83,7 @@ module.exports = options => {
 
 		return result;
 	};
+
 	proxy.unpipe = stream => {
 		piped.delete(stream);
 		return unpipe(stream);
diff --git a/source/errors.js b/source/errors.js
index fcf9548..b6cbadc 100644
--- a/source/errors.js
+++ b/source/errors.js
@@ -69,6 +69,7 @@ module.exports.HTTPError = class extends GotError {
 		} else {
 			statusMessage = http.STATUS_CODES[statusCode];
 		}
+
 		super(`Response code ${statusCode} (${statusMessage})`, {}, options);
 		this.name = 'HTTPError';
 		this.statusCode = statusCode;
diff --git a/source/known-hook-events.js b/source/known-hook-events.js
index 2fa0381..cd245e1 100644
--- a/source/known-hook-events.js
+++ b/source/known-hook-events.js
@@ -1,6 +1,8 @@
 'use strict';
 
 module.exports = [
+	'beforeError',
+	'init',
 	'beforeRequest',
 	'beforeRedirect',
 	'beforeRetry',
diff --git a/source/normalize-arguments.js b/source/normalize-arguments.js
index 2aa756c..92cabec 100644
--- a/source/normalize-arguments.js
+++ b/source/normalize-arguments.js
@@ -6,13 +6,21 @@ const urlParseLax = require('url-parse-lax');
 const lowercaseKeys = require('lowercase-keys');
 const urlToOptions = require('./utils/url-to-options');
 const isFormData = require('./utils/is-form-data');
+const validateSearchParams = require('./utils/validate-search-params');
 const merge = require('./merge');
 const knownHookEvents = require('./known-hook-events');
 
 const retryAfterStatusCodes = new Set([413, 429, 503]);
 
-// `preNormalize` handles static things (lowercasing headers; normalizing baseUrl, timeout, retry)
-// While `normalize` does `preNormalize` + handles things which need to be reworked when user changes them
+// `preNormalize` handles static options (e.g. headers).
+// For example, when you create a custom instance and make a request
+// with no static changes, they won't be normalized again.
+//
+// `normalize` operates on dynamic options - they cannot be saved.
+// For example, `body` is everytime different per request.
+// When it's done normalizing the new options, it performs merge()
+// on the prenormalized options and the normalized ones.
+
 const preNormalize = (options, defaults) => {
 	if (is.nullOrUndefined(options.headers)) {
 		options.headers = {};
@@ -49,6 +57,7 @@ const preNormalize = (options, defaults) => {
 	} else if (is.object(options.timeout)) {
 		options.gotTimeout = options.timeout;
 	}
+
 	delete options.timeout;
 
 	const {retry} = options;
@@ -125,6 +134,14 @@ const normalize = (url, options, defaults) => {
 	// Override both null/undefined with default protocol
 	options = merge({path: ''}, url, {protocol: url.protocol || 'https:'}, options);
 
+	for (const hook of options.hooks.init) {
+		const called = hook(options);
+
+		if (is.promise(called)) {
+			throw new TypeError('The `init` hook must be a synchronous function');
+		}
+	}
+
 	const {baseUrl} = options;
 	Object.defineProperty(options, 'baseUrl', {
 		set: () => {
@@ -136,8 +153,13 @@ const normalize = (url, options, defaults) => {
 	const {query} = options;
 	if (is.nonEmptyString(query) || is.nonEmptyObject(query) || query instanceof URLSearchParams) {
 		if (!is.string(query)) {
+			if (!(query instanceof URLSearchParams)) {
+				validateSearchParams(query);
+			}
+
 			options.query = (new URLSearchParams(query)).toString();
 		}
+
 		options.path = `${options.path.split('?')[0]}?${options.query}`;
 		delete options.query;
 	}
diff --git a/source/request-as-event-emitter.js b/source/request-as-event-emitter.js
index 7104627..79586af 100644
--- a/source/request-as-event-emitter.js
+++ b/source/request-as-event-emitter.js
@@ -33,6 +33,19 @@ module.exports = (options, input) => {
 	const getCookieString = options.cookieJar ? util.promisify(options.cookieJar.getCookieString.bind(options.cookieJar)) : null;
 	const agents = is.object(options.agent) ? options.agent : null;
 
+	const emitError = async error => {
+		try {
+			for (const hook of options.hooks.beforeError) {
+				// eslint-disable-next-line no-await-in-loop
+				error = await hook(error);
+			}
+
+			emitter.emit('error', error);
+		} catch (error2) {
+			emitter.emit('error', error2);
+		}
+	};
+
 	const get = async options => {
 		const currentUrl = redirectString || requestUrl;
 
@@ -141,7 +154,7 @@ module.exports = (options, input) => {
 
 				getResponse(response, options, emitter);
 			} catch (error) {
-				emitter.emit('error', error);
+				emitError(error);
 			}
 		};
 
@@ -166,7 +179,7 @@ module.exports = (options, input) => {
 				}
 
 				if (emitter.retry(error) === false) {
-					emitter.emit('error', error);
+					emitError(error);
 				}
 			});
 
@@ -198,7 +211,7 @@ module.exports = (options, input) => {
 					request.end(uploadComplete);
 				}
 			} catch (error) {
-				emitter.emit('error', new RequestError(error, options));
+				emitError(new RequestError(error, options));
 			}
 		};
 
@@ -208,9 +221,9 @@ module.exports = (options, input) => {
 
 			cacheRequest.once('error', error => {
 				if (error instanceof CacheableRequest.RequestError) {
-					emitter.emit('error', new RequestError(error, options));
+					emitError(new RequestError(error, options));
 				} else {
-					emitter.emit('error', new CacheError(error, options));
+					emitError(new CacheError(error, options));
 				}
 			});
 
@@ -220,7 +233,7 @@ module.exports = (options, input) => {
 			try {
 				handleRequest(fn.request(options, handleResponse));
 			} catch (error) {
-				emitter.emit('error', new RequestError(error, options));
+				emitError(new RequestError(error, options));
 			}
 		}
 	};
@@ -231,7 +244,7 @@ module.exports = (options, input) => {
 		try {
 			backoff = options.retry.retries(++retryCount, error);
 		} catch (error2) {
-			emitter.emit('error', error2);
+			emitError(error2);
 			return;
 		}
 
@@ -245,7 +258,7 @@ module.exports = (options, input) => {
 
 					await get(options);
 				} catch (error) {
-					emitter.emit('error', error);
+					emitError(error);
 				}
 			};
 
@@ -291,7 +304,7 @@ module.exports = (options, input) => {
 
 			await get(options);
 		} catch (error) {
-			emitter.emit('error', error);
+			emitError(error);
 		}
 	});
 
diff --git a/source/utils/timed-out.js b/source/utils/timed-out.js
index a0efde4..33611a7 100644
--- a/source/utils/timed-out.js
+++ b/source/utils/timed-out.js
@@ -82,11 +82,16 @@ module.exports = (request, delays, options) => {
 	}
 
 	if (delays.socket !== undefined) {
-		request.setTimeout(delays.socket, () => {
+		const socketTimeoutHandler = () => {
 			timeoutHandler(delays.socket, 'socket');
-		});
+		};
+
+		request.setTimeout(delays.socket, socketTimeoutHandler);
 
-		cancelers.push(() => request.setTimeout(0));
+		// `request.setTimeout(0)` causes a memory leak.
+		// We can just remove the listener and forget about the timer - it's unreffed.
+		// See https://github.com/sindresorhus/got/issues/690
+		cancelers.push(() => request.removeListener('timeout', socketTimeoutHandler));
 	}
 
 	if (delays.lookup !== undefined && !request.socketPath && !net.isIP(hostname || host)) {
diff --git a/source/utils/validate-search-params.js b/source/utils/validate-search-params.js
new file mode 100644
index 0000000..0f32893
--- /dev/null
+++ b/source/utils/validate-search-params.js
@@ -0,0 +1,15 @@
+'use strict';
+const is = require('@sindresorhus/is');
+
+module.exports = query => {
+	const verify = (value, type) => {
+		if (!is.string(value) && !is.number(value) && !is.boolean(value) && !is.null(value)) {
+			throw new TypeError(`The query ${type} '${value}' must be a string, number, boolean or null`);
+		}
+	};
+
+	for (const [key, value] of Object.entries(query)) {
+		verify(key, 'key');
+		verify(value, 'value');
+	}
+};


Test patch:
diff --git a/test/arguments.js b/test/arguments.js
index db62167..5d6ee7e 100644
--- a/test/arguments.js
+++ b/test/arguments.js
@@ -1,3 +1,4 @@
+/* eslint-disable node/no-deprecated-api */
 import {URL, URLSearchParams, parse} from 'url';
 import test from 'ava';
 import pEvent from 'p-event';
@@ -222,3 +223,19 @@ test('throws when trying to modify baseUrl after options got normalized', async
 
 	await t.throwsAsync(instanceA('/'), 'Failed to set baseUrl. Options are normalized already.');
 });
+
+test('throws if the query key is invalid', async t => {
+	await t.throwsAsync(() => got(s.url, {
+		query: {
+			[[]]: []
+		}
+	}), TypeError);
+});
+
+test('throws if the query value is invalid', async t => {
+	await t.throwsAsync(() => got(s.url, {
+		query: {
+			foo: []
+		}
+	}), TypeError);
+});
diff --git a/test/cache.js b/test/cache.js
index 979f102..7fd961b 100644
--- a/test/cache.js
+++ b/test/cache.js
@@ -40,6 +40,7 @@ test.before('setup', async () => {
 			response.setHeader('Location', `${s.url}/302`);
 			response.statusCode = 301;
 		}
+
 		response.end();
 		status301Index++;
 	});
@@ -51,6 +52,7 @@ test.before('setup', async () => {
 			response.setHeader('Location', `${s.url}/cache`);
 			response.statusCode = 302;
 		}
+
 		response.end();
 		status302Index++;
 	});
diff --git a/test/error.js b/test/error.js
index cb81312..749b91f 100644
--- a/test/error.js
+++ b/test/error.js
@@ -175,6 +175,7 @@ test('catch error in mimicResponse', async t => {
 	const mimicResponse = () => {
 		throw new Error('Error in mimic-response');
 	};
+
 	mimicResponse['@global'] = true;
 
 	const proxiedGot = proxyquire('..', {
diff --git a/test/hooks.js b/test/hooks.js
index b402a24..ee4b136 100644
--- a/test/hooks.js
+++ b/test/hooks.js
@@ -1,5 +1,6 @@
 import test from 'ava';
 import delay from 'delay';
+import getStream from 'get-stream';
 import {createServer} from './helpers/server';
 import got from '..';
 
@@ -18,6 +19,9 @@ test.before('setup', async () => {
 	};
 
 	s.on('/', echoHeaders);
+	s.on('/body', async (request, response) => {
+		response.end(await getStream(request));
+	});
 	s.on('/redirect', (request, response) => {
 		response.statusCode = 302;
 		response.setHeader('location', '/');
@@ -75,29 +79,65 @@ test('async hooks', async t => {
 	t.is(body.foo, 'bar');
 });
 
-test('catches thrown errors', async t => {
+test('catches init thrown errors', async t => {
 	await t.throwsAsync(() => got(s.url, {
 		hooks: {
-			beforeRequest: [
-				() => {
-					throw error;
-				}
-			]
+			init: [() => {
+				throw error;
+			}]
 		}
 	}), errorString);
 });
 
-test('catches promise rejections', async t => {
+test('catches beforeRequest thrown errors', async t => {
 	await t.throwsAsync(() => got(s.url, {
 		hooks: {
-			beforeRequest: [
-				() => Promise.reject(error)
-			]
+			beforeRequest: [() => {
+				throw error;
+			}]
+		}
+	}), errorString);
+});
+
+test('catches beforeRedirect thrown errors', async t => {
+	await t.throwsAsync(() => got(`${s.url}/redirect`, {
+		hooks: {
+			beforeRedirect: [() => {
+				throw error;
+			}]
+		}
+	}), errorString);
+});
+
+test('catches beforeRetry thrown errors', async t => {
+	await t.throwsAsync(() => got(`${s.url}/retry`, {
+		hooks: {
+			beforeRetry: [() => {
+				throw error;
+			}]
 		}
 	}), errorString);
 });
 
-test('catches beforeRequest errors', async t => {
+test('catches afterResponse thrown errors', async t => {
+	await t.throwsAsync(() => got(s.url, {
+		hooks: {
+			afterResponse: [() => {
+				throw error;
+			}]
+		}
+	}), errorString);
+});
+
+test('throws a helpful error when passing async function as init hook', async t => {
+	await t.throwsAsync(() => got(s.url, {
+		hooks: {
+			init: [() => Promise.resolve()]
+		}
+	}), 'The `init` hook must be a synchronous function');
+});
+
+test('catches beforeRequest promise rejections', async t => {
 	await t.throwsAsync(() => got(s.url, {
 		hooks: {
 			beforeRequest: [() => Promise.reject(error)]
@@ -105,7 +145,7 @@ test('catches beforeRequest errors', async t => {
 	}), errorString);
 });
 
-test('catches beforeRedirect errors', async t => {
+test('catches beforeRedirect promise rejections', async t => {
 	await t.throwsAsync(() => got(`${s.url}/redirect`, {
 		hooks: {
 			beforeRedirect: [() => Promise.reject(error)]
@@ -113,7 +153,7 @@ test('catches beforeRedirect errors', async t => {
 	}), errorString);
 });
 
-test('catches beforeRetry errors', async t => {
+test('catches beforeRetry promise rejections', async t => {
 	await t.throwsAsync(() => got(`${s.url}/retry`, {
 		hooks: {
 			beforeRetry: [() => Promise.reject(error)]
@@ -121,7 +161,7 @@ test('catches beforeRetry errors', async t => {
 	}), errorString);
 });
 
-test('catches afterResponse errors', async t => {
+test('catches afterResponse promise rejections', async t => {
 	await t.throwsAsync(() => got(s.url, {
 		hooks: {
 			afterResponse: [() => Promise.reject(error)]
@@ -129,7 +169,43 @@ test('catches afterResponse errors', async t => {
 	}), errorString);
 });
 
-test('beforeRequest', async t => {
+test('catches beforeError errors', async t => {
+	await t.throwsAsync(() => got(s.url, {
+		request: () => {},
+		hooks: {
+			beforeError: [() => Promise.reject(error)]
+		}
+	}), errorString);
+});
+
+test('init is called with options', async t => {
+	await got(s.url, {
+		json: true,
+		hooks: {
+			init: [
+				options => {
+					t.is(options.path, '/');
+					t.is(options.hostname, 'localhost');
+				}
+			]
+		}
+	});
+});
+
+test('init allows modifications', async t => {
+	const {body} = await got(`${s.url}/body`, {
+		hooks: {
+			init: [
+				options => {
+					options.body = 'foobar';
+				}
+			]
+		}
+	});
+	t.is(body, 'foobar');
+});
+
+test('beforeRequest is called with options', async t => {
 	await got(s.url, {
 		json: true,
 		hooks: {
@@ -157,7 +233,7 @@ test('beforeRequest allows modifications', async t => {
 	t.is(body.foo, 'bar');
 });
 
-test('beforeRedirect', async t => {
+test('beforeRedirect is called with options', async t => {
 	await got(`${s.url}/redirect`, {
 		json: true,
 		hooks: {
@@ -185,15 +261,17 @@ test('beforeRedirect allows modifications', async t => {
 	t.is(body.foo, 'bar');
 });
 
-test('beforeRetry', async t => {
+test('beforeRetry is called with options', async t => {
 	await got(`${s.url}/retry`, {
 		json: true,
 		retry: 1,
 		throwHttpErrors: false,
 		hooks: {
 			beforeRetry: [
-				options => {
+				(options, error, retryCount) => {
 					t.is(options.hostname, 'localhost');
+					t.truthy(error);
+					t.true(retryCount >= 1);
 				}
 			]
 		}
@@ -214,7 +292,7 @@ test('beforeRetry allows modifications', async t => {
 	t.is(body.foo, 'bar');
 });
 
-test('afterResponse', async t => {
+test('afterResponse is called with response', async t => {
 	await got(`${s.url}`, {
 		json: true,
 		hooks: {
@@ -330,3 +408,32 @@ test.serial('doesn\'t throw on afterResponse retry HTTP failure if throwHttpErro
 	});
 	t.is(statusCode, 500);
 });
+
+test('beforeError is called with an error', async t => {
+	await t.throwsAsync(() => got(s.url, {
+		request: () => {
+			throw error;
+		},
+		hooks: {
+			beforeError: [error2 => {
+				t.true(error2 instanceof Error);
+				return error2;
+			}]
+		}
+	}), errorString);
+});
+
+test('beforeError allows modifications', async t => {
+	const errorString2 = 'foobar';
+
+	await t.throwsAsync(() => got(s.url, {
+		request: () => {
+			throw error;
+		},
+		hooks: {
+			beforeError: [() => {
+				return new Error(errorString2);
+			}]
+		}
+	}), errorString2);
+});
diff --git a/test/redirects.js b/test/redirects.js
index dbff25f..4f09503 100644
--- a/test/redirects.js
+++ b/test/redirects.js
@@ -10,6 +10,7 @@ test.before('setup', async () => {
 	const reached = (request, response) => {
 		response.end('reached');
 	};
+
 	https = await createSSLServer();
 	http = await createServer();
 
diff --git a/test/socket-destroyed.js b/test/socket-destroyed.js
index e504a3c..038d2bb 100644
--- a/test/socket-destroyed.js
+++ b/test/socket-destroyed.js
@@ -1,6 +1,7 @@
 import test from 'ava';
 import got from '../source';
 
+// eslint-disable-next-line node/no-deprecated-api
 const {Timer} = process.binding('timer_wrap');
 
 test.serial('clear the progressInterval if the socket has been destroyed', async t => {
diff --git a/test/timeout.js b/test/timeout.js
index c36f6df..d98e955 100644
--- a/test/timeout.js
+++ b/test/timeout.js
@@ -18,6 +18,7 @@ const slowDataStream = () => {
 		if (count++ < 10) {
 			return slowStream.push('data\n'.repeat(100));
 		}
+
 		clearInterval(interval);
 		slowStream.push(null);
 	}, 100);
@@ -473,3 +474,21 @@ test('socket timeout is canceled on error', async t => {
 	// Wait a bit more to check if there are any unhandled errors
 	await delay(10);
 });
+
+test('no memory leak when using socket timeout and keepalive agent', async t => {
+	const promise = got(s.url, {
+		agent: keepAliveAgent,
+		timeout: {socket: requestDelay * 2}
+	});
+
+	let socket;
+	promise.on('request', request => {
+		request.on('socket', () => {
+			socket = request.socket;
+		});
+	});
+
+	await promise;
+
+	t.is(socket.listenerCount('timeout'), 0);
+});
diff --git a/test/url-to-options.js b/test/url-to-options.js
index ccffbe5..4692b4d 100644
--- a/test/url-to-options.js
+++ b/test/url-to-options.js
@@ -1,3 +1,4 @@
+/* eslint-disable node/no-deprecated-api */
 import url from 'url';
 import test from 'ava';
 import urlToOptions from '../source/utils/url-to-options';