Repository: sindresorhus/got
Pull request #595 (2018-08-29 12:03:16)

Add migration guides

sindresorhus: I think each migration should be a separate file.
szmarczak: This PR is not my first priority right now, so it may take a while to finish this.
sindresorhus: Can you link to this at the top of the readme with a link text like:

> Moving from Request?

Patch:
diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.md b/.github/ISSUE_TEMPLATE/1-bug-report.md
new file mode 100644
index 0000000..442cb03
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/1-bug-report.md
@@ -0,0 +1,38 @@
+---
+name: "🐞 Bug report"
+about: Something is not working as it should
+---
+
+#### Describe the bug
+
+ - Node.js version: 
+ - OS & version: 
+
+<!-- A clear and concise description of what the bug is. -->
+
+#### Actual behavior
+
+...
+
+#### Expected behavior
+
+...
+
+#### Code to reproduce
+
+```js
+...
+```
+
+<!--
+We encourage you to submit a pull request with a failing test:
+ - This will make it more likely for us to prioritize your issue.
+ - It's a good way to prove that the issue is related to Got and not your code. 
+
+Example: https://github.com/avajs/ava/blob/master/docs/01-writing-tests.md#failing-tests
+-->
+
+#### Checklist
+
+- [ ] I have read the documentation.
+- [ ] I have tried my code with the latest version of Node.js and Got.
diff --git a/.github/ISSUE_TEMPLATE/2-feature-request.md b/.github/ISSUE_TEMPLATE/2-feature-request.md
new file mode 100644
index 0000000..96adeaa
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/2-feature-request.md
@@ -0,0 +1,18 @@
+---
+name: "⭐ Feature request"
+about: Suggest an idea for Got
+---
+
+#### What problem are you trying to solve?
+
+...
+
+#### Describe the feature
+
+...
+
+<!-- Include a usage example of the feature. If the feature is currently possible with a workaround, include that too. -->
+
+#### Checklist
+
+- [ ] I have read the documentation and made sure this feature doesn't already exist.
diff --git a/.github/ISSUE_TEMPLATE/3-question.md b/.github/ISSUE_TEMPLATE/3-question.md
new file mode 100644
index 0000000..87211f0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/3-question.md
@@ -0,0 +1,12 @@
+---
+name: "❓ Question"
+about: Something is unclear or needs to be discussed
+---
+
+#### What would you like to discuss?
+
+...
+
+#### Checklist
+
+- [ ] I have read the documentation.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..9e9a219
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,6 @@
+#### Checklist
+
+- [ ] I have read the documentation.
+- [ ] I have included a pull request description of my changes.
+- [ ] I have included some tests.
+- [ ] If it's a new feature, I have included documentation updates.
diff --git a/migration-guides.md b/migration-guides.md
new file mode 100644
index 0000000..0aa6b7a
--- /dev/null
+++ b/migration-guides.md
@@ -0,0 +1,171 @@
+# Migration guides
+
+> :star: Switching from other HTTP request libraries to Got :star:
+
+### Migrating from Request
+
+You may think it's too hard to switch, but it's really not. 🦄
+
+Let's take the very first example from Request's readme:
+
+```js
+const request = require('request');
+
+request('https://google.com', (error, response, body) => {
+	console.log('error:', error); // Print the error if one occurred
+	console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
+	console.log('body:', body); // Print the HTML for the Google homepage
+});
+```
+
+With Got, it is:
+
+```js
+const got = require('got');
+
+(async () => {
+	try {
+		const response = await got('https://google.com');
+		console.log('statusCode:', response.statusCode);
+		console.log('body:', response.body);
+	} catch (error) {
+		console.log('error:', error);
+	}
+})();
+```
+
+Looks better now, huh? 😎
+
+#### Common options
+
+Both Request and Got accept [`http.request` options](https://nodejs.org/api/http.html#http_http_request_options_callback).
+
+These Got options are the same as with Request:
+
+- [`url`](https://github.com/sindresorhus/got#url) (+ we accept [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) instances too!)
+- [`body`](https://github.com/sindresorhus/got#body)
+- [`json`](https://github.com/sindresorhus/got#json)
+- [`followRedirect`](https://github.com/sindresorhus/got#followRedirect)
+- [`encoding`](https://github.com/sindresorhus/got#encoding)
+
+So if you're familiar with them, you're good to go.
+
+Oh, and one more thing... There's no `time` option. Assume [it's always true](https://github.com/sindresorhus/got#timings).
+
+#### Renamed options
+
+Readability is very important to us, so we have different names for these options:
+
+- `qs` → [`query`](https://github.com/sindresorhus/got#query)
+- `strictSSL` → [`rejectUnauthorized`](https://github.com/sindresorhus/got#rejectUnauthorized)
+- `gzip` → [`decompress`](https://github.com/sindresorhus/got#decompress)
+- `jar` → [`cookieJar`](https://github.com/sindresorhus/got#cookiejar) (accepts [`tough-cookie`](https://github.com/salesforce/tough-cookie) jar)
+
+It's more clear, isn't it?
+
+#### Changes in behavior
+
+The [`timeout` option](https://github.com/sindresorhus/got#timeout) has some extra features. You can [set timeouts on particular events](readme.md#timeout)!
+
+The [`query` option](https://github.com/sindresorhus/got#query) is always serialized using [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) unless it's a `string`.
+
+The [`baseUrl` option](https://github.com/sindresorhus/got#baseurl) appends the ending slash if it's not present.
+
+There's no `maxRedirects` option. It's always set to `10`.
+
+To use streams, just call `got.stream(url, options)` or `got(url, {stream: true, ...}`).
+
+#### Breaking changes
+
+- No `form` option. You have to pass a [`form-data` instance](https://github.com/form-data/form-data) through the [`body` option](https://github.com/sindresorhus/got#body).
+- No `oauth`/`hawk`/`aws`/`httpSignature` option. To sign requests, you need to create a [custom instance](advanced-creation.md#signing-requests).
+- No `agentClass`/`agentOptions`/`pool` option.
+- No `forever` option. You need to use [forever-agent](https://github.com/request/forever-agent).
+- No `proxy` option. You need to [pass a custom agent](readme.md#proxies).
+- No `removeRefererHeader` option. You can remove the referer header in a [`beforeRequest` hook](https://github.com/sindresorhus/got#hooksbeforeRequest):
+
+```js
+const gotInstance = got.extend({
+	hooks: {
+		beforeRequest: [
+			options => {
+				delete options.headers.referer;
+			}
+		]
+	}
+});
+
+gotInstance(url, options);
+```
+
+- No `jsonReviver`/`jsonReplacer` option, but you can use hooks for that too:
+
+```js
+const gotInstance = got.extend({
+	hooks: {
+		beforeRequest: [
+			options => {
+				if (options.json && options.jsonReplacer) {
+					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;
+					}
+
+					// #1 solution (faster)
+					options.body = newBody;
+
+					// #2 solution (slower)
+					options.body = JSON.parse(JSON.stringify(options.body, options.jsonReplacer));
+				}
+			}
+		],
+		afterResponse: [
+			response => {
+				// TODO in Got: We need to make the `options` public somehow
+				if (options.json && options.jsonReviver) {
+					response.body = JSON.stringify(JSON.parse(response.body, options.jsonReviver));
+				}
+
+				return response;
+			}
+		]
+	}
+});
+
+gotInstance(url, options);
+```
+
+Hooks are powerful, aren't they? [Read more](readme.md#hooks) to see what else you achieve using hooks.
+
+#### More about streams
+
+Let's take a quick look at another example from Request's readme:
+
+```js
+http.createServer((req, res) => {
+	if (req.url === '/doodle.png') {
+		req.pipe(request('https://example.com/doodle.png')).pipe(res);
+	}
+});
+```
+
+The cool feature here is that Request can proxy headers with the stream, but Got can do that too:
+
+```js
+http.createServer((req, res) => {
+	if (req.url === '/doodle.png') {
+		req.pipe(got.stream('https://example.com/doodle.png')).pipe(res);
+	}
+});
+```
+
+Nothing has really changed. Just remember to use `got.stream(url, options)` or `got(url, {stream: true, …`}). That's it!
+
+#### You're good to go!
+
+Well, you have already come this far. Take a look at the [documentation](readme.md#highlights). It's worth the time to read it. There are [some great tips](readme.md#aborting-the-request). If something is unclear or doesn't work as it should, don't hesitate to open an issue.
diff --git a/readme.md b/readme.md
index d137828..a270728 100644
--- a/readme.md
+++ b/readme.md
@@ -42,8 +42,9 @@ Got is for Node.js. For browsers, we recommend [Ky](https://github.com/sindresor
 - [Used by ~2000 packages and ~500K repos](https://github.com/sindresorhus/got/network/dependents)
 - Actively maintained
 
-[See how Got compares to other HTTP libraries](#comparison)
+[Moving from Request?](migration-guides.md)
 
+[See how Got compares to other HTTP libraries](#comparison)
 
 ## Install
 
@@ -1032,7 +1033,7 @@ const h2got = got.extend({request});
 | Electron support      |       ✔      |       ✖      |       ✖      |       ✖      |
 | Promise API           |       ✔      |       ✔      |       ✔      |       ✔      |
 | Stream API            |       ✔      |       ✔      | Node.js only |       ✖      |
-| Request cancelation   |       ✔      |       ✖      |       ✖      |       ✔      |
+| Request cancelation   |       ✔      |       ✖      |       ✔      |       ✔      |
 | RFC compliant caching |       ✔      |       ✖      |       ✖      |       ✖      |
 | Cookies (out-of-box)  |       ✔      |       ✔      |       ✖      |       ✖      |
 | Follows redirects     |       ✔      |       ✔      |       ✔      |       ✔      |
diff --git a/source/errors.js b/source/errors.js
index 70ed1f1..8030ee2 100644
--- a/source/errors.js
+++ b/source/errors.js
@@ -5,7 +5,7 @@ const PCancelable = require('p-cancelable');
 const is = require('@sindresorhus/is');
 
 class GotError extends Error {
-	constructor(message, error, opts) {
+	constructor(message, error, options) {
 		super(message);
 		Error.captureStackTrace(this, this.constructor);
 		this.name = 'GotError';
@@ -15,13 +15,13 @@ class GotError extends Error {
 		}
 
 		Object.assign(this, {
-			host: opts.host,
-			hostname: opts.hostname,
-			method: opts.method,
-			path: opts.path,
-			socketPath: opts.socketPath,
-			protocol: opts.protocol,
-			url: opts.href
+			host: options.host,
+			hostname: options.hostname,
+			method: options.method,
+			path: options.path,
+			socketPath: options.socketPath,
+			protocol: options.protocol,
+			url: options.href
 		});
 	}
 }
@@ -29,29 +29,29 @@ class GotError extends Error {
 module.exports.GotError = GotError;
 
 module.exports.CacheError = class extends GotError {
-	constructor(error, opts) {
-		super(error.message, error, opts);
+	constructor(error, options) {
+		super(error.message, error, options);
 		this.name = 'CacheError';
 	}
 };
 
 module.exports.RequestError = class extends GotError {
-	constructor(error, opts) {
-		super(error.message, error, opts);
+	constructor(error, options) {
+		super(error.message, error, options);
 		this.name = 'RequestError';
 	}
 };
 
 module.exports.ReadError = class extends GotError {
-	constructor(error, opts) {
-		super(error.message, error, opts);
+	constructor(error, options) {
+		super(error.message, error, options);
 		this.name = 'ReadError';
 	}
 };
 
 module.exports.ParseError = class extends GotError {
-	constructor(error, statusCode, opts, data) {
-		super(`${error.message} in "${urlLib.format(opts)}": \n${data.slice(0, 77)}...`, error, opts);
+	constructor(error, statusCode, options, data) {
+		super(`${error.message} in "${urlLib.format(options)}": \n${data.slice(0, 77)}...`, error, options);
 		this.name = 'ParseError';
 		this.statusCode = statusCode;
 		this.statusMessage = http.STATUS_CODES[this.statusCode];
@@ -59,7 +59,7 @@ module.exports.ParseError = class extends GotError {
 };
 
 module.exports.HTTPError = class extends GotError {
-	constructor(response, opts) {
+	constructor(response, options) {
 		const {statusCode} = response;
 		let {statusMessage} = response;
 
@@ -68,7 +68,7 @@ module.exports.HTTPError = class extends GotError {
 		} else {
 			statusMessage = http.STATUS_CODES[statusCode];
 		}
-		super(`Response code ${statusCode} (${statusMessage})`, {}, opts);
+		super(`Response code ${statusCode} (${statusMessage})`, {}, options);
 		this.name = 'HTTPError';
 		this.statusCode = statusCode;
 		this.statusMessage = statusMessage;
@@ -78,8 +78,8 @@ module.exports.HTTPError = class extends GotError {
 };
 
 module.exports.MaxRedirectsError = class extends GotError {
-	constructor(statusCode, redirectUrls, opts) {
-		super('Redirected 10 times. Aborting.', {}, opts);
+	constructor(statusCode, redirectUrls, options) {
+		super('Redirected 10 times. Aborting.', {}, options);
 		this.name = 'MaxRedirectsError';
 		this.statusCode = statusCode;
 		this.statusMessage = http.STATUS_CODES[this.statusCode];
@@ -88,15 +88,15 @@ module.exports.MaxRedirectsError = class extends GotError {
 };
 
 module.exports.UnsupportedProtocolError = class extends GotError {
-	constructor(opts) {
-		super(`Unsupported protocol "${opts.protocol}"`, {}, opts);
+	constructor(options) {
+		super(`Unsupported protocol "${options.protocol}"`, {}, options);
 		this.name = 'UnsupportedProtocolError';
 	}
 };
 
 module.exports.TimeoutError = class extends GotError {
-	constructor(error, opts) {
-		super(error.message, {code: 'ETIMEDOUT'}, opts);
+	constructor(error, options) {
+		super(error.message, {code: 'ETIMEDOUT'}, options);
 		this.name = 'TimeoutError';
 		this.event = error.event;
 	}
diff --git a/source/normalize-arguments.js b/source/normalize-arguments.js
index 37ed8db..3de0fab 100644
--- a/source/normalize-arguments.js
+++ b/source/normalize-arguments.js
@@ -96,7 +96,7 @@ const normalize = (url, options, defaults) => {
 	if (defaults) {
 		options = merge({}, defaults.options, options ? preNormalize(options, defaults.options) : {});
 	} else {
-		options = merge({}, options ? preNormalize(options) : {});
+		options = merge({}, preNormalize(options));
 	}
 
 	if (!is.string(url) && !is.object(url)) {
@@ -210,29 +210,27 @@ const normalize = (url, options, defaults) => {
 				return 0;
 			}
 
-			if (error !== null) {
-				if (!isRetryOnNetworkErrorAllowed(error) && (!options.retry.methods.has(error.method) || !options.retry.statusCodes.has(error.statusCode))) {
-					return 0;
-				}
-
-				if (Reflect.has(error, 'headers') && Reflect.has(error.headers, 'retry-after') && retryAfterStatusCodes.has(error.statusCode)) {
-					let after = Number(error.headers['retry-after']);
-					if (is.nan(after)) {
-						after = Date.parse(error.headers['retry-after']) - Date.now();
-					} else {
-						after *= 1000;
-					}
-
-					if (after > options.retry.maxRetryAfter) {
-						return 0;
-					}
+			if (!isRetryOnNetworkErrorAllowed(error) && (!options.retry.methods.has(error.method) || !options.retry.statusCodes.has(error.statusCode))) {
+				return 0;
+			}
 
-					return after;
+			if (Reflect.has(error, 'headers') && Reflect.has(error.headers, 'retry-after') && retryAfterStatusCodes.has(error.statusCode)) {
+				let after = Number(error.headers['retry-after']);
+				if (is.nan(after)) {
+					after = Date.parse(error.headers['retry-after']) - Date.now();
+				} else {
+					after *= 1000;
 				}
 
-				if (error.statusCode === 413) {
+				if (after > options.retry.maxRetryAfter) {
 					return 0;
 				}
+
+				return after;
+			}
+
+			if (error.statusCode === 413) {
+				return 0;
 			}
 
 			const noise = Math.random() * 100;
diff --git a/source/request-as-event-emitter.js b/source/request-as-event-emitter.js
index c579575..d13d793 100644
--- a/source/request-as-event-emitter.js
+++ b/source/request-as-event-emitter.js
@@ -119,19 +119,19 @@ module.exports = (options, input) => {
 
 						redirects.push(redirectString);
 
-						const redirectOpts = {
+						const redirectOptions = {
 							...options,
 							...urlToOptions(redirectURL)
 						};
 
 						for (const hook of options.hooks.beforeRedirect) {
 							// eslint-disable-next-line no-await-in-loop
-							await hook(redirectOpts);
+							await hook(redirectOptions);
 						}
 
-						emitter.emit('redirect', response, redirectOpts);
+						emitter.emit('redirect', response, redirectOptions);
 
-						await get(redirectOpts);
+						await get(redirectOptions);
 						return;
 					}
 				}
@@ -201,9 +201,9 @@ module.exports = (options, input) => {
 
 		if (options.cache) {
 			const cacheableRequest = new CacheableRequest(fn.request, options.cache);
-			const cacheReq = cacheableRequest(options, handleResponse);
+			const cacheRequest = cacheableRequest(options, handleResponse);
 
-			cacheReq.once('error', error => {
+			cacheRequest.once('error', error => {
 				if (error instanceof CacheableRequest.RequestError) {
 					emitter.emit('error', new RequestError(error, options));
 				} else {
@@ -211,7 +211,7 @@ module.exports = (options, input) => {
 				}
 			});
 
-			cacheReq.once('request', handleRequest);
+			cacheRequest.once('request', handleRequest);
 		} else {
 			// Catches errors thrown by calling fn.request(...)
 			try {
diff --git a/source/utils/timed-out.js b/source/utils/timed-out.js
index d993231..a0efde4 100644
--- a/source/utils/timed-out.js
+++ b/source/utils/timed-out.js
@@ -85,6 +85,8 @@ module.exports = (request, delays, options) => {
 		request.setTimeout(delays.socket, () => {
 			timeoutHandler(delays.socket, 'socket');
 		});
+
+		cancelers.push(() => request.setTimeout(0));
 	}
 
 	if (delays.lookup !== undefined && !request.socketPath && !net.isIP(hostname || host)) {


Test patch:
diff --git a/test/arguments.js b/test/arguments.js
index e6c73ea..f2383ef 100644
--- a/test/arguments.js
+++ b/test/arguments.js
@@ -36,19 +36,27 @@ test.after('cleanup', async () => {
 });
 
 test('url is required', async t => {
-	const error = await t.throwsAsync(got());
-	t.is(error.message, 'Parameter `url` must be a string or object, not undefined');
+	await t.throwsAsync(
+		got(),
+		{
+			message: 'Parameter `url` must be a string or object, not undefined'
+		}
+	);
 });
 
 test('url should be utf-8 encoded', async t => {
-	const error = await t.throwsAsync(got(`${s.url}/%D2%E0%EB%EB%E8%ED`));
-	t.is(error.message, 'URI malformed');
+	await t.throwsAsync(
+		got(`${s.url}/%D2%E0%EB%EB%E8%ED`),
+		{
+			message: 'URI malformed'
+		}
+	);
 });
 
 test('string url with query is preserved', async t => {
 	const path = '/?test=http://example.com?foo=bar';
-	const response = await got(`${s.url}${path}`);
-	t.is(response.body, path);
+	const {body} = await got(`${s.url}${path}`);
+	t.is(body, path);
 });
 
 test('options are optional', async t => {
@@ -81,11 +89,13 @@ test('requestUrl with url.parse object as first argument', async t => {
 	t.is((await got(parse(`${s.url}/test`))).requestUrl, `${s.url}/test`);
 });
 
-test('overrides querystring from opts', async t => {
-	const response = await got(
+test('overrides querystring from options', async t => {
+	const {body} = await got(
 		`${s.url}/?drop=this`,
 		{
-			query: {test: 'wow'},
+			query: {
+				test: 'wow'
+			},
 			cache: {
 				get(key) {
 					t.is(key, `cacheable-request:GET:${s.url}/?test=wow`);
@@ -96,16 +106,18 @@ test('overrides querystring from opts', async t => {
 			}
 		}
 	);
-	t.is(response.body, '/?test=wow');
+
+	t.is(body, '/?test=wow');
 });
 
 test('escapes query parameter values', async t => {
-	const response = await got(`${s.url}`, {
+	const {body} = await got(`${s.url}`, {
 		query: {
 			test: 'it’s ok'
 		}
 	});
-	t.is(response.body, '/?test=it%E2%80%99s+ok');
+
+	t.is(body, '/?test=it%E2%80%99s+ok');
 });
 
 test('the `query` option can be a URLSearchParams', async t => {
@@ -119,18 +131,24 @@ test('should ignore empty query object', async t => {
 });
 
 test('should throw with auth in url string', async t => {
-	const error = await t.throwsAsync(got('https://test:45d3ps453@account.myservice.com/api/token'));
-	t.is(error.message, 'Basic authentication must be done with the `auth` option');
+	await t.throwsAsync(
+		got('https://test:45d3ps453@account.myservice.com/api/token'),
+		{
+			message: 'Basic authentication must be done with the `auth` option'
+		}
+	);
 });
 
 test('does not throw with auth in url object', async t => {
-	await t.notThrowsAsync(got({
-		auth: 'foo:bar',
-		hostname: s.host,
-		port: s.port,
-		protocol: 'http:',
-		path: '/test'
-	}));
+	await t.notThrowsAsync(
+		got({
+			auth: 'foo:bar',
+			hostname: s.host,
+			port: s.port,
+			protocol: 'http:',
+			path: '/test'
+		})
+	);
 });
 
 test('should throw when body is set to object', async t => {
diff --git a/test/cache.js b/test/cache.js
index 8b7477a..0c2dc55 100644
--- a/test/cache.js
+++ b/test/cache.js
@@ -122,7 +122,7 @@ test('doesn\'t cache response when received HTTP error', async t => {
 	const endpoint = '/first-error';
 	const cache = new Map();
 
-	const response = await got(s.url + endpoint, {cache, throwHttpErrors: false});
-	t.is(response.statusCode, 200);
-	t.deepEqual(response.body, 'ok');
+	const {statusCode, body} = await got(s.url + endpoint, {cache, throwHttpErrors: false});
+	t.is(statusCode, 200);
+	t.deepEqual(body, 'ok');
 });
diff --git a/test/cookies.js b/test/cookies.js
index 9823a9e..b5c052c 100644
--- a/test/cookies.js
+++ b/test/cookies.js
@@ -108,15 +108,16 @@ test('no unhandled errors', async t => {
 		connection.end('blah');
 	}).listen(0);
 
-	const error = 'snap!';
+	const message = 'snap!';
 
-	const opts = {
+	const options = {
 		cookieJar: {
 			setCookie: () => {},
-			getCookieString: (_, __, cb) => cb(new Error(error))
+			getCookieString: (_, __, cb) => cb(new Error(message))
 		}
 	};
-	await t.throwsAsync(got(`http://127.0.0.1:${server.address().port}`, opts), error);
+
+	await t.throwsAsync(got(`http://127.0.0.1:${server.address().port}`, options), {message});
 	await delay(500);
 	t.pass();
 
diff --git a/test/error.js b/test/error.js
index 32347f8..a0bc2ed 100644
--- a/test/error.js
+++ b/test/error.js
@@ -123,24 +123,30 @@ test('http.request error', async t => {
 		request: () => {
 			throw new TypeError('The header content contains invalid characters');
 		}
-	}), {instanceOf: got.RequestError, message: 'The header content contains invalid characters'});
+	}), {
+		instanceOf: got.RequestError,
+		message: 'The header content contains invalid characters'
+	});
 });
 
 test('http.request pipe error', async t => {
-	const error = 'snap!';
+	const message = 'snap!';
 
 	await t.throwsAsync(got(s.url, {
-		request: (...opts) => {
-			const modified = http.request(...opts);
+		request: (...options) => {
+			const modified = http.request(...options);
 			modified.end = () => {
 				modified.abort();
-				throw new Error(error);
+				throw new Error(message);
 			};
 
 			return modified;
 		},
 		throwHttpErrors: false
-	}), {instanceOf: got.RequestError, message: error});
+	}), {
+		instanceOf: got.RequestError,
+		message
+	});
 });
 
 test('http.request error through CacheableRequest', async t => {
@@ -149,7 +155,10 @@ test('http.request error through CacheableRequest', async t => {
 			throw new TypeError('The header content contains invalid characters');
 		},
 		cache: new Map()
-	}), {instanceOf: got.RequestError, message: 'The header content contains invalid characters'});
+	}), {
+		instanceOf: got.RequestError,
+		message: 'The header content contains invalid characters'
+	});
 });
 
 test('catch error in mimicResponse', async t => {
diff --git a/test/gzip.js b/test/gzip.js
index 44569ca..617d044 100644
--- a/test/gzip.js
+++ b/test/gzip.js
@@ -78,13 +78,13 @@ test('handles gzip error - stream', async t => {
 });
 
 test('decompress option opts out of decompressing', async t => {
-	const response = await got(s.url, {decompress: false});
-	t.true(Buffer.compare(response.body, gzipData) === 0);
+	const {body} = await got(s.url, {decompress: false});
+	t.is(Buffer.compare(body, gzipData), 0);
 });
 
 test('decompress option doesn\'t alter encoding of uncompressed responses', async t => {
-	const response = await got(`${s.url}/uncompressed`, {decompress: false});
-	t.is(response.body, testContentUncompressed);
+	const {body} = await got(`${s.url}/uncompressed`, {decompress: false});
+	t.is(body, testContentUncompressed);
 });
 
 test('preserve headers property', async t => {
diff --git a/test/hooks.js b/test/hooks.js
index 6b3324a..b402a24 100644
--- a/test/hooks.js
+++ b/test/hooks.js
@@ -61,7 +61,7 @@ test.after('cleanup', async () => {
 });
 
 test('async hooks', async t => {
-	const response = await got(s.url, {
+	const {body} = await got(s.url, {
 		json: true,
 		hooks: {
 			beforeRequest: [
@@ -72,7 +72,7 @@ test('async hooks', async t => {
 			]
 		}
 	});
-	t.is(response.body.foo, 'bar');
+	t.is(body.foo, 'bar');
 });
 
 test('catches thrown errors', async t => {
@@ -144,7 +144,7 @@ test('beforeRequest', async t => {
 });
 
 test('beforeRequest allows modifications', async t => {
-	const response = await got(s.url, {
+	const {body} = await got(s.url, {
 		json: true,
 		hooks: {
 			beforeRequest: [
@@ -154,7 +154,7 @@ test('beforeRequest allows modifications', async t => {
 			]
 		}
 	});
-	t.is(response.body.foo, 'bar');
+	t.is(body.foo, 'bar');
 });
 
 test('beforeRedirect', async t => {
@@ -172,7 +172,7 @@ test('beforeRedirect', async t => {
 });
 
 test('beforeRedirect allows modifications', async t => {
-	const response = await got(`${s.url}/redirect`, {
+	const {body} = await got(`${s.url}/redirect`, {
 		json: true,
 		hooks: {
 			beforeRedirect: [
@@ -182,7 +182,7 @@ test('beforeRedirect allows modifications', async t => {
 			]
 		}
 	});
-	t.is(response.body.foo, 'bar');
+	t.is(body.foo, 'bar');
 });
 
 test('beforeRetry', async t => {
@@ -201,7 +201,7 @@ test('beforeRetry', async t => {
 });
 
 test('beforeRetry allows modifications', async t => {
-	const response = await got(`${s.url}/retry`, {
+	const {body} = await got(`${s.url}/retry`, {
 		json: true,
 		hooks: {
 			beforeRetry: [
@@ -211,7 +211,7 @@ test('beforeRetry allows modifications', async t => {
 			]
 		}
 	});
-	t.is(response.body.foo, 'bar');
+	t.is(body.foo, 'bar');
 });
 
 test('afterResponse', async t => {
@@ -230,7 +230,7 @@ test('afterResponse', async t => {
 });
 
 test('afterResponse allows modifications', async t => {
-	const response = await got(`${s.url}`, {
+	const {body} = await got(`${s.url}`, {
 		json: true,
 		hooks: {
 			afterResponse: [
@@ -242,11 +242,11 @@ test('afterResponse allows modifications', async t => {
 			]
 		}
 	});
-	t.is(response.body.hello, 'world');
+	t.is(body.hello, 'world');
 });
 
 test('afterResponse allows to retry', async t => {
-	const response = await got(`${s.url}/401`, {
+	const {statusCode} = await got(`${s.url}/401`, {
 		hooks: {
 			afterResponse: [
 				(response, retryWithMergedOptions) => {
@@ -263,7 +263,7 @@ test('afterResponse allows to retry', async t => {
 			]
 		}
 	});
-	t.is(response.statusCode, 200);
+	t.is(statusCode, 200);
 });
 
 test('no infinity loop when retrying on afterResponse', async t => {
@@ -309,7 +309,7 @@ test.serial('throws on afterResponse retry failure', async t => {
 test.serial('doesn\'t throw on afterResponse retry HTTP failure if throwHttpErrors is false', async t => {
 	visited401then500 = false;
 
-	const response = await got(`${s.url}/401then500`, {
+	const {statusCode} = await got(`${s.url}/401then500`, {
 		throwHttpErrors: false,
 		retry: 1,
 		hooks: {
@@ -328,5 +328,5 @@ test.serial('doesn\'t throw on afterResponse retry HTTP failure if throwHttpErro
 			]
 		}
 	});
-	t.is(response.statusCode, 500);
+	t.is(statusCode, 500);
 });
diff --git a/test/http.js b/test/http.js
index cd29c2b..f0d6482 100644
--- a/test/http.js
+++ b/test/http.js
@@ -57,11 +57,11 @@ test('error with code', async t => {
 });
 
 test('status code 304 doesn\'t throw', async t => {
-	const p = got(`${s.url}/304`);
-	await t.notThrowsAsync(p);
-	const response = await p;
-	t.is(response.statusCode, 304);
-	t.is(response.body, '');
+	const promise = got(`${s.url}/304`);
+	await t.notThrowsAsync(promise);
+	const {statusCode, body} = await promise;
+	t.is(statusCode, 304);
+	t.is(body, '');
 });
 
 test('doesn\'t throw on throwHttpErrors === false', async t => {
diff --git a/test/progress.js b/test/progress.js
index 8526621..32351cf 100644
--- a/test/progress.js
+++ b/test/progress.js
@@ -71,10 +71,10 @@ test.after('cleanup', async () => {
 test('download progress', async t => {
 	const events = [];
 
-	const response = await got(`${s.url}/download`, {encoding: null})
+	const {body} = await got(`${s.url}/download`, {encoding: null})
 		.on('downloadProgress', event => events.push(event));
 
-	checkEvents(t, events, response.body.length);
+	checkEvents(t, events, body.length);
 });
 
 test('download progress - missing total size', async t => {
diff --git a/test/stream.js b/test/stream.js
index de57d28..d4956a7 100644
--- a/test/stream.js
+++ b/test/stream.js
@@ -72,13 +72,13 @@ test('have request event', async t => {
 });
 
 test('have redirect event', async t => {
-	const response = await pEvent(got.stream(`${s.url}/redirect`), 'redirect');
-	t.is(response.headers.location, s.url);
+	const {headers} = await pEvent(got.stream(`${s.url}/redirect`), 'redirect');
+	t.is(headers.location, s.url);
 });
 
 test('have response event', async t => {
-	const response = await pEvent(got.stream(s.url), 'response');
-	t.is(response.statusCode, 200);
+	const {statusCode} = await pEvent(got.stream(s.url), 'response');
+	t.is(statusCode, 200);
 });
 
 test('have error event', async t => {
@@ -92,8 +92,8 @@ test('have error event #2', async t => {
 });
 
 test('have response event on throwHttpErrors === false', async t => {
-	const response = await pEvent(got.stream(`${s.url}/error`, {throwHttpErrors: false}), 'response');
-	t.is(response.statusCode, 404);
+	const {statusCode} = await pEvent(got.stream(`${s.url}/error`, {throwHttpErrors: false}), 'response');
+	t.is(statusCode, 404);
 });
 
 test('accepts option.body as Stream', async t => {
@@ -103,8 +103,8 @@ test('accepts option.body as Stream', async t => {
 });
 
 test('redirect response contains old url', async t => {
-	const response = await pEvent(got.stream(`${s.url}/redirect`), 'response');
-	t.is(response.requestUrl, `${s.url}/redirect`);
+	const {requestUrl} = await pEvent(got.stream(`${s.url}/redirect`), 'response');
+	t.is(requestUrl, `${s.url}/redirect`);
 });
 
 test('check for pipe method', t => {
@@ -184,10 +184,9 @@ test('proxies content-encoding header when options.decompress is false', async t
 
 test('destroying got.stream() cancels the request', async t => {
 	const stream = got.stream(s.url);
-
-	const req = await pEvent(stream, 'request');
+	const request = await pEvent(stream, 'request');
 	stream.destroy();
-	t.truthy(req.aborted);
+	t.truthy(request.aborted);
 });
 
 test('piping to got.stream.put()', async t => {
diff --git a/test/timeout.js b/test/timeout.js
index 6cff58e..c36f6df 100644
--- a/test/timeout.js
+++ b/test/timeout.js
@@ -458,3 +458,18 @@ test('no more timeouts after an error', async t => {
 	// Wait a bit more to check if there are any unhandled errors
 	await delay(10);
 });
+
+test('socket timeout is canceled on error', async t => {
+	const message = 'oh, snap!';
+
+	const promise = got(s.url, {
+		timeout: {socket: requestTimeout},
+		retry: 0
+	}).on('request', request => {
+		request.emit('error', new Error(message));
+	});
+
+	await t.throwsAsync(promise, {message});
+	// Wait a bit more to check if there are any unhandled errors
+	await delay(10);
+});