Repository: sindresorhus/got (ts)

Issue:
Option to not retry on timeout errors
I'm configuring requests for AWS lambda environment, and case is that:
- I set timeout option to ensure it ends before allowed lambda invocation time ends (so I handle eventual timeout gently by rejecting lambda invocation with meaningful error), and automated retry after such timeout occurs is unwanted.
- I'd like to have retries applied for case of temporary network or server unavailability errors.

Is there an option to achieve that?

Pull request description:
Add ability to specify which network error codes to retry on
Fixes #654 

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/advanced-creation.md b/advanced-creation.md
index 0b78e94..86de39a 100644
--- a/advanced-creation.md
+++ b/advanced-creation.md
@@ -82,6 +82,16 @@ const defaults = {
 				502,
 				503,
 				504
+			],
+			errorCodes: [
+				'ETIMEDOUT',
+				'ECONNRESET',
+				'EADDRINUSE',
+				'ECONNREFUSED',
+				'EPIPE',
+				'ENOTFOUND',
+				'ENETUNREACH',
+				'EAI_AGAIN'
 			]
 		},
 		headers: {
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 255c96b..c54d48d 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
 
@@ -258,8 +259,9 @@ Default:
 - methods: `GET` `PUT` `HEAD` `DELETE` `OPTIONS` `TRACE`
 - statusCodes: [`408`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) [`413`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413) [`429`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) [`500`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) [`502`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) [`503`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) [`504`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504)
 - maxRetryAfter: `undefined`
+- errorCodes: `ETIMEDOUT` `ECONNRESET` `EADDRINUSE` `ECONNREFUSED` `EPIPE` `ENOTFOUND` `ENETUNREACH` `EAI_AGAIN`
 
-An object representing `retries`, `methods`, `statusCodes` and `maxRetryAfter` fields for the time until retry, allowed methods, allowed status codes and maximum [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) time.
+An object representing `retries`, `methods`, `statusCodes`, `maxRetryAfter` and `errorCodes` fields for the time until retry, allowed methods, allowed status codes, maximum [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) time and allowed error codes.
 
 If `maxRetryAfter` is set to `undefined`, it will use `options.timeout`.<br>
 If [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header is greater than `maxRetryAfter`, it will cancel the request.
@@ -268,7 +270,7 @@ Delays between retries counts with function `1000 * Math.pow(2, retry) + Math.ra
 
 The `retries` property can be a `number` or a `function` with `retry` and `error` arguments. The function must return a delay in milliseconds (`0` return value cancels retry).
 
-**Note:** It retries only on the specified methods, status codes, and on these network errors:
+**Note:** By default, it retries *only* on the specified methods, status codes, and on these network errors:
 - `ETIMEDOUT`: One of the [timeout](#timeout) limits were reached.
 - `ECONNRESET`: Connection was forcibly closed by a peer.
 - `EADDRINUSE`: Could not bind to any free port.
@@ -451,6 +453,14 @@ const instance = got.extend({
 
 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.
 
+##### request
+
+Type: `Object`
+
+**Note:** This is not a [http.ClientRequest](https://nodejs.org/api/http.html#http_class_http_clientrequest).
+
+- `gotOptions` - The options that were set on this request.
+
 ##### body
 
 Type: `string` `Object` *(depending on `options.json`)*
@@ -661,7 +671,7 @@ The default Got options.
 
 ## Errors
 
-Each error contains (if available) `body`, `statusCode`, `statusMessage`, `host`, `hostname`, `method`, `path`, `protocol` and `url` properties to make debugging easier.
+Each error contains (if available) `body`, `statusCode`, `statusMessage`, `host`, `hostname`, `method`, `path`, `protocol`, `url`, and `gotOptions` properties to make debugging easier.
 
 In Promise mode, the `response` is attached to the error.
 
diff --git a/source/errors.js b/source/errors.js
index 70ed1f1..fcf9548 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,14 @@ 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,
+			gotOptions: options
 		});
 	}
 }
@@ -29,29 +30,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 +60,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 +69,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 +79,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 +89,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/index.js b/source/index.js
index e835998..cbf7c37 100644
--- a/source/index.js
+++ b/source/index.js
@@ -22,6 +22,16 @@ const defaults = {
 				502,
 				503,
 				504
+			],
+			errorCodes: [
+				'ETIMEDOUT',
+				'ECONNRESET',
+				'EADDRINUSE',
+				'ECONNREFUSED',
+				'EPIPE',
+				'ENOTFOUND',
+				'ENETUNREACH',
+				'EAI_AGAIN'
 			]
 		},
 		headers: {
diff --git a/source/normalize-arguments.js b/source/normalize-arguments.js
index 3de0fab..7790c4f 100644
--- a/source/normalize-arguments.js
+++ b/source/normalize-arguments.js
@@ -4,7 +4,6 @@ const urlLib = require('url');
 const is = require('@sindresorhus/is');
 const urlParseLax = require('url-parse-lax');
 const lowercaseKeys = require('lowercase-keys');
-const isRetryOnNetworkErrorAllowed = require('./utils/is-retry-on-network-error-allowed');
 const urlToOptions = require('./utils/url-to-options');
 const isFormData = require('./utils/is-form-data');
 const merge = require('./merge');
@@ -56,7 +55,8 @@ const preNormalize = (options, defaults) => {
 	options.retry = {
 		retries: 0,
 		methods: [],
-		statusCodes: []
+		statusCodes: [],
+		errorCodes: []
 	};
 
 	if (is.nonEmptyObject(defaults) && retry !== false) {
@@ -83,6 +83,10 @@ const preNormalize = (options, defaults) => {
 		options.retry.statusCodes = new Set(options.retry.statusCodes);
 	}
 
+	if (is.array(options.retry.errorCodes)) {
+		options.retry.errorCodes = new Set(options.retry.errorCodes);
+	}
+
 	return options;
 };
 
@@ -210,7 +214,7 @@ const normalize = (url, options, defaults) => {
 				return 0;
 			}
 
-			if (!isRetryOnNetworkErrorAllowed(error) && (!options.retry.methods.has(error.method) || !options.retry.statusCodes.has(error.statusCode))) {
+			if ((!error || !options.retry.errorCodes.has(error.code)) && (!options.retry.methods.has(error.method) || !options.retry.statusCodes.has(error.statusCode))) {
 				return 0;
 			}
 
diff --git a/source/request-as-event-emitter.js b/source/request-as-event-emitter.js
index c579575..7104627 100644
--- a/source/request-as-event-emitter.js
+++ b/source/request-as-event-emitter.js
@@ -92,6 +92,9 @@ module.exports = (options, input) => {
 				response.retryCount = retryCount;
 				response.timings = timings;
 				response.redirectUrls = redirects;
+				response.request = {
+					gotOptions: options
+				};
 
 				const rawCookies = response.headers['set-cookie'];
 				if (options.cookieJar && rawCookies) {
@@ -119,19 +122,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 +204,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 +214,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/is-retry-on-network-error-allowed.js b/source/utils/is-retry-on-network-error-allowed.js
deleted file mode 100644
index 4556988..0000000
--- a/source/utils/is-retry-on-network-error-allowed.js
+++ /dev/null
@@ -1,20 +0,0 @@
-'use strict';
-
-const WHITELIST = new Set([
-	'ETIMEDOUT',
-	'ECONNRESET',
-	'EADDRINUSE',
-	'ECONNREFUSED',
-	'EPIPE',
-	'ENOTFOUND',
-	'ENETUNREACH',
-	'EAI_AGAIN'
-]);
-
-module.exports = error => {
-	if (error && WHITELIST.has(error.code)) {
-		return true;
-	}
-
-	return false;
-};
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..979f102 100644
--- a/test/cache.js
+++ b/test/cache.js
@@ -110,6 +110,21 @@ test('Redirects are cached and re-used internally', async t => {
 	t.is(firstResponse.body, secondResponse.body);
 });
 
+test('Cached response should have got options', async t => {
+	const endpoint = '/cache';
+	const cache = new Map();
+	const options = {
+		url: s.url + endpoint,
+		auth: 'foo:bar',
+		cache
+	};
+
+	await got(options);
+	const secondResponse = await got(options);
+
+	t.is(secondResponse.request.gotOptions.auth, options.auth);
+});
+
 test('Cache error throws got.CacheError', async t => {
 	const endpoint = '/no-store';
 	const cache = {};
@@ -122,7 +137,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..cb81312 100644
--- a/test/error.js
+++ b/test/error.js
@@ -112,6 +112,16 @@ test('custom body', async t => {
 	t.is(error.body, 'not');
 });
 
+test('contains Got options', async t => {
+	const options = {
+		url: s.url,
+		auth: 'foo:bar'
+	};
+
+	const error = await t.throwsAsync(got(options));
+	t.is(error.gotOptions.auth, options.auth);
+});
+
 test('no status message is overriden by the default one', async t => {
 	const error = await t.throwsAsync(got(`${s.url}/no-status-message`));
 	t.is(error.statusCode, 400);
@@ -123,24 +133,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 +165,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..1b86ae9 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 => {
@@ -91,3 +91,12 @@ test('requestUrl response when sending url as param', async t => {
 test('response contains url', async t => {
 	t.is((await got(s.url)).url, `${s.url}/`);
 });
+
+test('response contains got options', async t => {
+	const options = {
+		url: s.url,
+		auth: 'foo:bar'
+	};
+
+	t.is((await got(options)).request.gotOptions.auth, options.auth);
+});
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/retry.js b/test/retry.js
index 5626b10..ac23e72 100644
--- a/test/retry.js
+++ b/test/retry.js
@@ -1,3 +1,4 @@
+import http from 'http';
 import test from 'ava';
 import pEvent from 'p-event';
 import got from '../source';
@@ -151,6 +152,38 @@ test('custom retries', async t => {
 	t.true(tried);
 });
 
+test('custom errors', async t => {
+	const errorCode = 'OH_SNAP';
+
+	let isTried = false;
+	const error = await t.throwsAsync(got(`${s.url}/500`, {
+		request: (...args) => {
+			const request = http.request(...args);
+			if (!isTried) {
+				isTried = true;
+				const error = new Error('Snap!');
+				error.code = errorCode;
+
+				setTimeout(() => request.emit('error', error));
+			}
+
+			return request;
+		},
+		retry: {
+			retries: 1,
+			methods: [
+				'GET'
+			],
+			errorCodes: [
+				errorCode
+			]
+		}
+	}));
+
+	t.is(error.statusCode, 500);
+	t.true(isTried);
+});
+
 test('respect 413 Retry-After', async t => {
 	const {statusCode, body} = await got(`${s.url}/measure413`, {
 		throwHttpErrors: false,
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);
+});