Repository: sindresorhus/got
Pull request #922 (2019-11-05 22:10:46)

Add package.json funding property
**npm** just added a new [`npm fund` command](https://blog.npmjs.org/post/188841555980/updates-to-community-docs-more) in it's [v6.13.0](https://github.com/npm/cli/releases/tag/v6.13.0) release as part of the efforts to help out the OSS community.

This PR adds `funding` info to **got** so that users depending on it can actually retrieve the funding url when they run `npm fund` in their projects.

Note: The funding information will only be available once a new version of the package is published to the npm registry.


Patch:
diff --git a/.github/funding.yml b/.github/funding.yml
index 2760f0b..bacefef 100644
--- a/.github/funding.yml
+++ b/.github/funding.yml
@@ -1,2 +1,2 @@
-github: sindresorhus
+github: [sindresorhus, szmarczak]
 tidelift: npm/got
diff --git a/documentation/advanced-creation.md b/documentation/advanced-creation.md
index 640b319..482db69 100644
--- a/documentation/advanced-creation.md
+++ b/documentation/advanced-creation.md
@@ -2,119 +2,6 @@
 
 > Make calling REST APIs easier by creating niche-specific `got` instances.
 
-#### got.create(settings)
-
-Example: [gh-got](https://github.com/sindresorhus/gh-got/blob/master/index.js)
-
-Configures a new `got` instance with the provided settings. You can access the resolved options with the `.defaults` property on the instance.
-
-**Note:** In contrast to [`got.extend()`](../readme.md#gotextendinstances), this method has no defaults.
-
-##### [options](readme.md#options)
-
-To inherit from the parent, set it to `got.defaults.options` or use [`got.mergeOptions(defaults.options, options)`](../readme.md#gotmergeoptionsparentoptions-newoptions).<br>
-**Note:** Avoid using [object spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals) as it doesn't work recursively.
-
-##### mutableDefaults
-
-Type: `boolean`<br>
-Default: `false`
-
-States if the defaults are mutable. It can be useful when you need to [update headers over time](readme.md#hooksafterresponse), for example, update an access token when it expires.
-
-##### handlers
-
-Type: `Function[]`<br>
-Default: `[]`
-
-An array of functions. You execute them directly by calling `got()`. They are some sort of "global hooks" - these functions are called first. The last handler (*it's hidden*) is either [`asPromise`](../source/as-promise.ts) or [`asStream`](../source/as-stream.ts), depending on the `options.stream` property.
-
-To inherit from the parent, set it as `got.defaults.handlers`.<br>
-To use the default handler, just omit specifying this.
-
-Each handler takes two arguments:
-
-###### [options](readme.md#options)
-
-**Note:** These options are [normalized](source/normalize-arguments.js).
-
-###### next()
-
-Returns a `Promise` or a `Stream` depending on [`options.stream`](readme.md#stream).
-
-```js
-const settings = {
-	handlers: [
-		(options, next) => {
-			if (options.stream) {
-				// It's a Stream, so we can perform stream-specific actions on it
-				return next(options)
-					.on('request', request => {
-						setTimeout(() => {
-							request.abort();
-						}, 50);
-					});
-			}
-
-			// It's a Promise
-			return next(options);
-		}
-	],
-	options: got.mergeOptions(got.defaults.options, {
-		responseType: 'json'
-	})
-};
-
-const jsonGot = got.create(settings);
-```
-
-Sometimes you don't need to use `got.create(defaults)`. You should go for `got.extend(options)` if you don't want to overwrite the defaults:
-
-```js
-const settings = {
-	handler: got.defaults.handler,
-	options: got.mergeOptions(got.defaults.options, {
-		headers: {
-			unicorn: 'rainbow'
-		}
-	})
-};
-
-const unicorn = got.create(settings);
-
-// Same as:
-const unicorn = got.extend({headers: {unicorn: 'rainbow'}});
-```
-
-**Note:** Handlers can be asynchronous. The recommended approach is:
-
-```js
-const handler = (options, next) => {
-	if (options.stream) {
-		// It's a Stream
-		return next(options);
-	}
-
-	// It's a Promise
-	return (async () => {
-		try {
-			const response = await next(options);
-
-			response.yourOwnProperty = true;
-
-			return response;
-		} catch (error) {
-			// Every error will be replaced by this one.
-			// Before you receive any error here,
-			// it will be passed to the `beforeError` hooks first.
-
-			// Note: this one won't be passed to `beforeError` hook. It's final.
-			throw new Error('Your very own error.');
-		}
-	})();
-};
-```
-
 ### Merging instances
 
 Got supports composing multiple instances together. This is very powerful. You can create a client that limits download speed and then compose it with an instance that signs a request. It's like plugins without any of the plugin mess. You just create instances and then compose them together.
diff --git a/documentation/examples/gh-got.js b/documentation/examples/gh-got.js
index 0834382..51a234c 100644
--- a/documentation/examples/gh-got.js
+++ b/documentation/examples/gh-got.js
@@ -24,7 +24,7 @@ const instance = got.extend({
 			}
 
 			// Don't touch streams
-			if (options.stream) {
+			if (options.isStream) {
 				return next(options);
 			}
 
diff --git a/documentation/lets-make-a-plugin.md b/documentation/lets-make-a-plugin.md
index 5ab4353..8ba883b 100644
--- a/documentation/lets-make-a-plugin.md
+++ b/documentation/lets-make-a-plugin.md
@@ -128,7 +128,7 @@ We should name our errors, just to know if the error is from the API response. S
 			}
 
 			// Don't touch streams
-			if (options.stream) {
+			if (options.isStream) {
 				return next(options);
 			}
 
@@ -197,7 +197,7 @@ const getRateLimit = ({headers}) => ({
 			}
 
 			// Don't touch streams
-			if (options.stream) {
+			if (options.isStream) {
 				return next(options);
 			}
 
diff --git a/package.json b/package.json
index c2a0b33..d20794e 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,10 @@
 {
 	"name": "got",
-	"version": "10.0.0-alpha.3",
+	"version": "10.0.0-beta.1",
 	"description": "Simplified HTTP requests",
 	"license": "MIT",
 	"repository": "sindresorhus/got",
+	"funding": "https://github.com/sindresorhus/got?sponsor=1",
 	"main": "dist/source",
 	"engines": {
 		"node": ">=10"
@@ -38,7 +39,7 @@
 	],
 	"dependencies": {
 		"@sindresorhus/is": "^1.0.0",
-		"@szmarczak/http-timer": "^3.0.0",
+		"@szmarczak/http-timer": "^3.1.0",
 		"@types/cacheable-request": "^6.0.1",
 		"@types/tough-cookie": "^2.3.5",
 		"cacheable-lookup": "^0.2.1",
@@ -56,22 +57,22 @@
 	"devDependencies": {
 		"@sindresorhus/tsconfig": "^0.5.0",
 		"@types/duplexer3": "^0.1.0",
-		"@types/node": "^12.7.5",
+		"@types/node": "^12.12.8",
 		"@types/sinon": "^7.0.13",
-		"@typescript-eslint/eslint-plugin": "^2.3.1",
-		"@typescript-eslint/parser": "^2.3.1",
+		"@typescript-eslint/eslint-plugin": "^2.7.0",
+		"@typescript-eslint/parser": "^2.7.0",
 		"ava": "^2.4.0",
 		"coveralls": "^3.0.4",
 		"create-test-server": "^3.0.1",
 		"del-cli": "^3.0.0",
 		"delay": "^4.3.0",
-		"eslint-config-xo-typescript": "^0.19.0",
-		"form-data": "^2.5.1",
+		"eslint-config-xo-typescript": "^0.21.0",
+		"form-data": "^3.0.0",
 		"get-port": "^5.0.0",
-		"keyv": "^3.1.0",
+		"keyv": "^4.0.0",
 		"lolex": "^5.1.1",
 		"nock": "^11.3.4",
-		"np": "^5.0.3",
+		"np": "^5.1.3",
 		"nyc": "^14.0.0",
 		"p-event": "^4.0.0",
 		"proxyquire": "^2.0.1",
@@ -79,8 +80,8 @@
 		"slow-stream": "0.0.4",
 		"tempy": "^0.3.0",
 		"tough-cookie": "^3.0.0",
-		"ts-node": "^8.4.1",
-		"typescript": "^3.6.4",
+		"ts-node": "^8.5.2",
+		"typescript": "^3.7.2",
 		"xo": "^0.25.3"
 	},
 	"types": "dist/source",
@@ -115,9 +116,7 @@
 		],
 		"rules": {
 			"@typescript-eslint/explicit-function-return-type": "off",
-			"@typescript-eslint/promise-function-async": "off",
-			"@typescript-eslint/strict-boolean-expressions": "off",
-			"@typescript-eslint/no-unnecessary-condition": "off"
+			"@typescript-eslint/promise-function-async": "off"
 		},
 		"ignores": [
 			"documentation/examples/*"
diff --git a/readme.md b/readme.md
index 4897c4c..b182f0c 100644
--- a/readme.md
+++ b/readme.md
@@ -108,7 +108,7 @@ It's a `GET` request by default, but can be changed by using different methods o
 
 #### got([url], [options])
 
-Returns a Promise for a [`response` object](#response) or a [stream](#streams-1) if `options.stream` is set to true.
+Returns a Promise for a [`response` object](#response) or a [stream](#streams-1) if `options.isStream` is set to true.
 
 ##### url
 
@@ -138,12 +138,29 @@ Useful when used with `got.extend()` to create niche-specific Got-instances.
 
 **Note:** `prefixUrl` will be ignored if the `url` argument is a URL instance.
 
+**Tip:** If the input URL still contains the initial `prefixUrl`, you can change it as many times as you want. Otherwise it will throw an error.
+
 ```js
 const got = require('got');
 
 (async () => {
 	await got('unicorn', {prefixUrl: 'https://cats.com'});
 	//=> 'https://cats.com/unicorn'
+
+	const instance = got.extend({
+		prefixUrl: 'https://google.com'
+	});
+
+	await instance('unicorn', {
+		hooks: {
+			beforeRequest: [
+				options => {
+					options.prefixUrl = 'https://cats.com';
+				}
+			]
+		}
+	});
+	//=> 'https://cats.com/unicorn'
 })();
 ```
 
@@ -154,9 +171,9 @@ Default: `{}`
 
 Request headers.
 
-Existing headers will be overwritten. Headers set to `null` will be omitted.
+Existing headers will be overwritten. Headers set to `undefined` will be omitted.
 
-###### stream
+###### isStream
 
 Type: `boolean`<br>
 Default: `false`
@@ -225,11 +242,18 @@ const instance = got.extend({
 ###### responseType
 
 Type: `string`<br>
-Default: `text`
+Default: `'default'`
 
 **Note:** When using streams, this option is ignored.
 
-Parsing method used to retrieve the body from the response. Can be `text`, `json` or `buffer`. The promise has `.json()` and `.buffer()` and `.text()` functions which set this option automatically.
+Parsing method used to retrieve the body from the response.
+
+- `'default'` - if `options.encoding` is `null`, the body will be a Buffer. Otherwise it will be a string unless it's overwritten in a `afterResponse` hook,
+- `'text'` - will always give a string, no matter what's the `options.encoding` or if the body is a custom object,
+- `'json'` - will always give an object, unless it's invalid JSON - then it will throw.
+- `'buffer'` - will always give a Buffer, no matter what's the `options.encoding`. It will throw if the body is a custom object.
+
+The promise has `.json()` and `.buffer()` and `.text()` functions which set this option automatically.
 
 Example:
 
@@ -246,11 +270,23 @@ When set to `true` the promise will return the [Response body](#body-1) instead
 
 ###### cookieJar
 
-Type: [`tough.CookieJar` instance](https://github.com/salesforce/tough-cookie#cookiejar)
+Type: `object` | [`tough.CookieJar` instance](https://github.com/salesforce/tough-cookie#cookiejar)
 
 **Note:** If you provide this option, `options.headers.cookie` will be overridden.
 
-Cookie support. You don't have to care about parsing or how to store them. [Example.](#cookies)
+Cookie support. You don't have to care about parsing or how to store them. [Example](#cookies).
+
+###### cookieJar.setCookie
+
+Type: `Function<Promise>`
+
+The function takes two arguments: `rawCookie` (`string`) and `url` (`string`).
+
+###### cookieJar.getCookieString
+
+Type: `Function<Promise>`
+
+The function takes one argument: `url` (`string`).
 
 ###### ignoreInvalidCookies
 
@@ -280,8 +316,6 @@ If set to `true` and the `Content-Type` header is not set, it will be set to `ap
 
 Type: `string | object<string, string | number> | URLSearchParams`
 
-**Note:** The `query` option was renamed to `searchParams` in Got v10. The `query` option name is still functional, but is being deprecated and will be completely removed in Got v11.
-
 Query string that will be added to the request URL. This will override the query string in `url`.
 
 If you need to pass in an array, you can do it using a `URLSearchParams` instance:
@@ -518,6 +552,8 @@ got.post('https://example.com', {
 });
 ```
 
+**Note:** When retrying in a `afterResponse` hook, all remaining `beforeRetry` hooks will be called without the `error` and `retryCount` arguments.
+
 ###### hooks.afterResponse
 
 Type: `Function[]`<br>
@@ -525,7 +561,7 @@ Default: `[]`
 
 **Note:** When using streams, this hook is ignored.
 
-Called with [response object](#response) and a retry function.
+Called with [response object](#response) and a retry function. Calling the retry function will trigger `beforeRetry` hooks.
 
 Each function should return the response. This is especially useful when you want to refresh an access token. Example:
 
@@ -553,6 +589,11 @@ const instance = got.extend({
 				// No changes otherwise
 				return response;
 			}
+		],
+		beforeRetry: [
+			(options, error, retryCount) => {
+				// This will be called on `retryWithMergedOptions(...)`
+			}
 		]
 	},
 	mutableDefaults: true
@@ -677,7 +718,7 @@ The number of times the request was retried.
 
 #### got.stream(url, [options])
 
-Sets `options.stream` to `true`.
+Sets `options.isStream` to `true`.
 
 Returns a [duplex stream](https://nodejs.org/api/stream.html#stream_class_stream_duplex) with additional events:
 
@@ -789,8 +830,6 @@ client.get('/demo');
 })();
 ```
 
-**Tip:** Need more control over the behavior of Got? Check out the [`got.create()`](documentation/advanced-creation.md).
-
 Additionally, `got.extend()` accepts two properties from the `defaults` object: `mutableDefaults` and `handlers`. Example:
 
 ```js
@@ -808,6 +847,34 @@ const mergedHandlers = got.extend({
 });
 ```
 
+**Note:** Handlers can be asynchronous. The recommended approach is:
+
+```js
+const handler = (options, next) => {
+	if (options.stream) {
+		// It's a Stream
+		return next(options);
+	}
+
+	// It's a Promise
+	return (async () => {
+		try {
+			const response = await next(options);
+			response.yourOwnProperty = true;
+			return response;
+		} catch (error) {
+			// Every error will be replaced by this one.
+			// Before you receive any error here,
+			// it will be passed to the `beforeError` hooks first.
+			// Note: this one won't be passed to `beforeError` hook. It's final.
+			throw new Error('Your very own error.');
+		}
+	})();
+};
+
+const instance = got.extend({handlers: [handler]});
+```
+
 #### got.extend(...instances)
 
 Merges many instances into a single one:
@@ -862,7 +929,57 @@ Options are deeply merged to a new object. The value of each key is determined a
 
 Type: `object`
 
-The default Got options used in that instance.
+The Got defaults used in that instance.
+
+##### [options](#options)
+
+##### handlers
+
+Type: `Function[]`<br>
+Default: `[]`
+
+An array of functions. You execute them directly by calling `got()`. They are some sort of "global hooks" - these functions are called first. The last handler (*it's hidden*) is either [`asPromise`](source/as-promise.ts) or [`asStream`](source/as-stream.ts), depending on the `options.isStream` property.
+
+Each handler takes two arguments:
+
+###### [options](#options)
+
+###### next()
+
+Returns a `Promise` or a `Stream` depending on [`options.isStream`](#isstream).
+
+```js
+const settings = {
+	handlers: [
+		(options, next) => {
+			if (options.isStream) {
+				// It's a Stream, so we can perform stream-specific actions on it
+				return next(options)
+					.on('request', request => {
+						setTimeout(() => {
+							request.abort();
+						}, 50);
+					});
+			}
+
+			// It's a Promise
+			return next(options);
+		}
+	],
+	options: got.mergeOptions(got.defaults.options, {
+		responseType: 'json'
+	})
+};
+
+const jsonGot = got.create(settings);
+```
+
+##### mutableDefaults
+
+Type: `boolean`<br>
+Default: `false`
+
+A read-only boolean describing whether the defaults are mutable or not. If set to `true`, you can [update headers over time](#hooksafterresponse), for example, update an access token when it expires.
 
 ## Errors
 
@@ -1229,7 +1346,7 @@ const got = require('got');
 
 ### User Agent
 
-It's a good idea to set the `'user-agent'` header so the provider can more easily see how their resource is used. By default, it's the URL to this repo. You can omit this header by setting it to `null`.
+It's a good idea to set the `'user-agent'` header so the provider can more easily see how their resource is used. By default, it's the URL to this repo. You can omit this header by setting it to `undefined`.
 
 ```js
 const got = require('got');
@@ -1243,7 +1360,7 @@ got('https://sindresorhus.com', {
 
 got('https://sindresorhus.com', {
 	headers: {
-		'user-agent': null
+		'user-agent': undefined
 	}
 });
 ```
diff --git a/source/as-promise.ts b/source/as-promise.ts
index 51a69ac..df4d9a1 100644
--- a/source/as-promise.ts
+++ b/source/as-promise.ts
@@ -4,29 +4,36 @@ import getStream = require('get-stream');
 import is from '@sindresorhus/is';
 import PCancelable = require('p-cancelable');
 import {NormalizedOptions, Response, CancelableRequest} from './utils/types';
-import {mergeOptions} from './merge';
 import {ParseError, ReadError, HTTPError} from './errors';
-import {reNormalizeArguments} from './normalize-arguments';
-import requestAsEventEmitter from './request-as-event-emitter';
+import requestAsEventEmitter, {proxyEvents} from './request-as-event-emitter';
+import {normalizeArguments, mergeOptions} from './normalize-arguments';
 
-type ResponseReturn = Response | Buffer | string | any;
+const parseBody = (body: Response['body'], responseType: NormalizedOptions['responseType'], statusCode: Response['statusCode']) => {
+	if (responseType === 'json' && is.string(body)) {
+		return statusCode === 204 ? '' : JSON.parse(body);
+	}
 
-export const isProxiedSymbol = Symbol('proxied');
+	if (responseType === 'buffer' && is.string(body)) {
+		return Buffer.from(body);
+	}
 
-export default function asPromise(options: NormalizedOptions): CancelableRequest<Response> {
-	const proxy = new EventEmitter();
+	if (responseType === 'text') {
+		return String(body);
+	}
 
-	const parseBody = (response: Response): void => {
-		if (options.responseType === 'json') {
-			response.body = JSON.parse(response.body);
-		} else if (options.responseType === 'buffer') {
-			response.body = Buffer.from(response.body);
-		} else if (options.responseType !== 'text' && !is.falsy(options.responseType)) {
-			throw new Error(`Failed to parse body of type '${options.responseType}'`);
-		}
-	};
+	if (responseType === 'default') {
+		return body;
+	}
+
+	throw new Error(`Failed to parse body of type '${typeof body}' as '${responseType}'`);
+};
 
-	const promise = new PCancelable<IncomingMessage>((resolve, reject, onCancel) => {
+export default function asPromise(options: NormalizedOptions) {
+	const proxy = new EventEmitter();
+	let finalResponse: Pick<Response, 'body' | 'statusCode'>;
+
+	// @ts-ignore `.json()`, `.buffer()` and `.text()` are added later
+	const promise = new PCancelable<IncomingMessage | Response['body']>((resolve, reject, onCancel) => {
 		const emitter = requestAsEventEmitter(options);
 		onCancel(emitter.abort);
 
@@ -46,32 +53,27 @@ export default function asPromise(options: NormalizedOptions): CancelableRequest
 		emitter.on('response', async (response: Response) => {
 			proxy.emit('response', response);
 
-			const stream = is.null_(options.encoding) ? getStream.buffer(response) : getStream(response, {encoding: options.encoding});
+			const streamAsPromise = is.null_(options.encoding) ? getStream.buffer(response) : getStream(response, {encoding: options.encoding});
 
-			let data: Buffer | string;
 			try {
-				data = await stream;
+				response.body = await streamAsPromise;
 			} catch (error) {
 				emitError(new ReadError(error, options));
 				return;
 			}
 
-			if (response.req && response.req.aborted) {
-				// Canceled while downloading - will throw a CancelError or TimeoutError
+			if (response.req?.aborted) {
+				// Canceled while downloading - will throw a `CancelError` or `TimeoutError` error
 				return;
 			}
 
-			const limitStatusCode = options.followRedirect ? 299 : 399;
-
-			response.body = data;
-
 			try {
 				for (const [index, hook] of options.hooks.afterResponse.entries()) {
+					// @ts-ignore
 					// eslint-disable-next-line no-await-in-loop
-					response = await hook(response, updatedOptions => {
-						updatedOptions = reNormalizeArguments(mergeOptions(options, {
+					response = await hook(response, async (updatedOptions: NormalizedOptions) => {
+						updatedOptions = normalizeArguments(mergeOptions(options, {
 							...updatedOptions,
-							// @ts-ignore TS complaining that it's missing properties, which get merged
 							retry: {
 								calculateDelay: () => 0
 							},
@@ -84,7 +86,19 @@ export default function asPromise(options: NormalizedOptions): CancelableRequest
 						// The loop continues. We don't want duplicates (asPromise recursion).
 						updatedOptions.hooks.afterResponse = options.hooks.afterResponse.slice(0, index);
 
-						return asPromise(updatedOptions);
+						for (const hook of options.hooks.beforeRetry) {
+							// eslint-disable-next-line no-await-in-loop
+							await hook(updatedOptions);
+						}
+
+						const promise = asPromise(updatedOptions);
+
+						onCancel(() => {
+							promise.catch(() => {});
+							promise.cancel();
+						});
+
+						return promise;
 					});
 				}
 			} catch (error) {
@@ -94,18 +108,22 @@ export default function asPromise(options: NormalizedOptions): CancelableRequest
 
 			const {statusCode} = response;
 
-			if (response.body) {
-				try {
-					parseBody(response);
-				} catch (error) {
-					if (statusCode >= 200 && statusCode < 300) {
-						const parseError = new ParseError(error, response, options);
-						emitError(parseError);
-						return;
-					}
+			finalResponse = {
+				body: response.body,
+				statusCode
+			};
+
+			try {
+				response.body = parseBody(response.body, options.responseType, response.statusCode);
+			} catch (error) {
+				if (statusCode >= 200 && statusCode < 300) {
+					const parseError = new ParseError(error, response, options);
+					emitError(parseError);
+					return;
 				}
 			}
 
+			const limitStatusCode = options.followRedirect ? 299 : 399;
 			if (statusCode !== 304 && (statusCode < 200 || statusCode > limitStatusCode)) {
 				const error = new HTTPError(response, options);
 				if (emitter.retry(error) === false) {
@@ -124,40 +142,35 @@ export default function asPromise(options: NormalizedOptions): CancelableRequest
 		});
 
 		emitter.once('error', reject);
-		[
-			'request',
-			'redirect',
-			'uploadProgress',
-			'downloadProgress'
-		].forEach(event => emitter.on(event, (...args: unknown[]) => {
-			proxy.emit(event, ...args);
-		}));
-	}) as CancelableRequest<ResponseReturn>;
-
-	promise[isProxiedSymbol] = true;
-
-	promise.on = (name, fn) => {
+
+		proxyEvents(proxy, emitter);
+	}) as CancelableRequest<any>;
+
+	promise.on = (name: string, fn: (...args: any[]) => void) => {
 		proxy.on(name, fn);
 		return promise;
 	};
 
-	promise.json = () => {
-		options.responseType = 'json';
-		options.resolveBodyOnly = true;
-		return promise;
-	};
+	const shortcut = (responseType: NormalizedOptions['responseType']): CancelableRequest<any> => {
+		// eslint-disable-next-line promise/prefer-await-to-then
+		const newPromise = promise.then(() => parseBody(finalResponse.body, responseType, finalResponse.statusCode));
 
-	promise.buffer = () => {
-		options.responseType = 'buffer';
-		options.resolveBodyOnly = true;
-		return promise;
+		Object.defineProperties(newPromise, Object.getOwnPropertyDescriptors(promise));
+
+		// @ts-ignore The missing properties are added above
+		return newPromise;
 	};
 
-	promise.text = () => {
-		options.responseType = 'text';
-		options.resolveBodyOnly = true;
-		return promise;
+	promise.json = () => {
+		if (is.undefined(options.headers.accept)) {
+			options.headers.accept = 'application/json';
+		}
+
+		return shortcut('json');
 	};
 
+	promise.buffer = () => shortcut('buffer');
+	promise.text = () => shortcut('text');
+
 	return promise;
 }
diff --git a/source/as-stream.ts b/source/as-stream.ts
index 38cd947..8a2fd90 100644
--- a/source/as-stream.ts
+++ b/source/as-stream.ts
@@ -2,11 +2,11 @@ import {PassThrough as PassThroughStream, Duplex as DuplexStream} from 'stream';
 import stream = require('stream');
 import {IncomingMessage} from 'http';
 import duplexer3 = require('duplexer3');
-import requestAsEventEmitter from './request-as-event-emitter';
+import requestAsEventEmitter, {proxyEvents} from './request-as-event-emitter';
 import {HTTPError, ReadError} from './errors';
-import {NormalizedOptions, Response} from './utils/types';
+import {NormalizedOptions, Response, GotEvents} from './utils/types';
 
-export class ProxyStream extends DuplexStream {
+export class ProxyStream extends DuplexStream implements GotEvents<ProxyStream> {
 	isFromCache?: boolean;
 }
 
@@ -14,7 +14,7 @@ export default function asStream(options: NormalizedOptions): ProxyStream {
 	const input = new PassThroughStream();
 	const output = new PassThroughStream();
 	const proxy = duplexer3(input, output) as ProxyStream;
-	const piped = new Set<any>(); // TODO: Should be `new Set<ProxyStream>();`.
+	const piped = new Set<any>(); // TODO: Should be `new Set<stream.Writable>();`.
 	let isFinished = false;
 
 	options.retry.calculateDelay = () => 0;
@@ -24,9 +24,16 @@ export default function asStream(options: NormalizedOptions): ProxyStream {
 			proxy.destroy();
 			throw new Error('Got\'s stream is not writable when the `body` option is used');
 		};
+	} else if (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH') {
+		options.body = input;
+	} else {
+		proxy.write = () => {
+			proxy.destroy();
+			throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
+		};
 	}
 
-	const emitter = requestAsEventEmitter(options, input);
+	const emitter = requestAsEventEmitter(options);
 
 	const emitError = async (error: Error): Promise<void> => {
 		try {
@@ -82,8 +89,8 @@ export default function asStream(options: NormalizedOptions): ProxyStream {
 			for (const [key, value] of Object.entries(response.headers)) {
 				// Got gives *decompressed* data. Overriding `content-encoding` header would result in an error.
 				// It's not possible to decompress already decompressed data, is it?
-				const allowed = options.decompress ? key !== 'content-encoding' : true;
-				if (allowed) {
+				const isAllowed = options.decompress ? key !== 'content-encoding' : true;
+				if (isAllowed) {
 					destination.setHeader(key, value);
 				}
 			}
@@ -94,19 +101,8 @@ export default function asStream(options: NormalizedOptions): ProxyStream {
 		proxy.emit('response', response);
 	});
 
-	const events = [
-		'error',
-		'request',
-		'redirect',
-		'uploadProgress',
-		'downloadProgress'
-	];
-
-	for (const event of events) {
-		emitter.on(event, (...args) => {
-			proxy.emit(event, ...args);
-		});
-	}
+	proxyEvents(proxy, emitter);
+	emitter.on('error', (error: Error) => proxy.emit('error', error));
 
 	const pipe = proxy.pipe.bind(proxy);
 	const unpipe = proxy.unpipe.bind(proxy);
diff --git a/source/calculate-retry-delay.ts b/source/calculate-retry-delay.ts
index 9fc2ecd..1c7f14b 100644
--- a/source/calculate-retry-delay.ts
+++ b/source/calculate-retry-delay.ts
@@ -16,7 +16,7 @@ const calculateRetryDelay: RetryFunction = ({attemptCount, retryOptions, error})
 		return 0;
 	}
 
-	const {response} = error as HTTPError | ParseError | MaxRedirectsError;
+	const {response} = error as HTTPError | ParseError | MaxRedirectsError | undefined;
 	if (response && Reflect.has(response.headers, 'retry-after') && retryAfterStatusCodes.has(response.statusCode)) {
 		let after = Number(response.headers['retry-after']);
 		if (is.nan(after)) {
@@ -32,7 +32,7 @@ const calculateRetryDelay: RetryFunction = ({attemptCount, retryOptions, error})
 		return after;
 	}
 
-	if (response && response.statusCode === 413) {
+	if (response?.statusCode === 413) {
 		return 0;
 	}
 
diff --git a/source/create.ts b/source/create.ts
index 55687f6..85366e9 100644
--- a/source/create.ts
+++ b/source/create.ts
@@ -1,3 +1,4 @@
+import {Merge} from 'type-fest';
 import * as errors from './errors';
 import {
 	Options,
@@ -6,27 +7,51 @@ import {
 	Response,
 	CancelableRequest,
 	URLOrOptions,
-	URLArgument,
 	HandlerFunction,
-	ExtendedOptions,
-	NormalizedDefaults
+	ExtendedOptions
 } from './utils/types';
 import deepFreeze from './utils/deep-freeze';
-import merge, {mergeOptions} from './merge';
-import asPromise, {isProxiedSymbol} from './as-promise';
+import asPromise from './as-promise';
 import asStream, {ProxyStream} from './as-stream';
-import {preNormalizeArguments, normalizeArguments} from './normalize-arguments';
+import {normalizeArguments, mergeOptions} from './normalize-arguments';
 import {Hooks} from './known-hook-events';
 
-export type HTTPAlias = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete';
+export type HTTPAlias =
+	| 'get'
+	| 'post'
+	| 'put'
+	| 'patch'
+	| 'head'
+	| 'delete';
 
-export type ReturnResponse = (url: URLArgument | Options & { stream?: false; url: URLArgument }, options?: Options & { stream?: false }) => CancelableRequest<Response>;
-export type ReturnStream = (url: URLArgument | Options & { stream: true; url: URLArgument }, options?: Options & { stream: true }) => ProxyStream;
+export type ReturnStream = (url: string | Options & {isStream: true}, options?: Options & {isStream: true}) => ProxyStream;
 export type GotReturn = ProxyStream | CancelableRequest<Response>;
 
-const getPromiseOrStream = (options: NormalizedOptions): GotReturn => options.stream ? asStream(options) : asPromise(options);
+const getPromiseOrStream = (options: NormalizedOptions): GotReturn => options.isStream ? asStream(options) : asPromise(options);
 
-export interface Got extends Record<HTTPAlias, ReturnResponse> {
+type OptionsOfDefaultResponseBody = Options & {isStream?: false; resolveBodyOnly?: false; responseType?: 'default'};
+type OptionsOfTextResponseBody = Options & {isStream?: false; resolveBodyOnly?: false; responseType: 'text'};
+type OptionsOfJSONResponseBody = Options & {isStream?: false; resolveBodyOnly?: false; responseType: 'json'};
+type OptionsOfBufferResponseBody = Options & {isStream?: false; resolveBodyOnly?: false; responseType: 'buffer'};
+type ResponseBodyOnly = {resolveBodyOnly: true};
+
+interface GotFunctions {
+	// `asPromise` usage
+	(url: string | OptionsOfDefaultResponseBody, options?: OptionsOfDefaultResponseBody): CancelableRequest<Response>;
+	(url: string | OptionsOfTextResponseBody, options?: OptionsOfTextResponseBody): CancelableRequest<Response<string>>;
+	(url: string | OptionsOfJSONResponseBody, options?: OptionsOfJSONResponseBody): CancelableRequest<Response<object>>;
+	(url: string | OptionsOfBufferResponseBody, options?: OptionsOfBufferResponseBody): CancelableRequest<Response<Buffer>>;
+
+	(url: string | OptionsOfDefaultResponseBody & ResponseBodyOnly, options?: OptionsOfDefaultResponseBody & ResponseBodyOnly): CancelableRequest<any>;
+	(url: string | OptionsOfTextResponseBody & ResponseBodyOnly, options?: OptionsOfTextResponseBody & ResponseBodyOnly): CancelableRequest<string>;
+	(url: string | OptionsOfJSONResponseBody & ResponseBodyOnly, options?: OptionsOfJSONResponseBody & ResponseBodyOnly): CancelableRequest<object>;
+	(url: string | OptionsOfBufferResponseBody & ResponseBodyOnly, options?: OptionsOfBufferResponseBody & ResponseBodyOnly): CancelableRequest<Buffer>;
+
+	// `asStream` usage
+	(url: string | Options & {isStream: true}, options?: Options & {isStream: true}): ProxyStream;
+}
+
+export interface Got extends Merge<Record<HTTPAlias, GotFunctions>, GotFunctions> {
 	stream: GotStream;
 	defaults: Defaults | Readonly<Defaults>;
 	GotError: typeof errors.GotError;
@@ -40,13 +65,9 @@ export interface Got extends Record<HTTPAlias, ReturnResponse> {
 	TimeoutError: typeof errors.TimeoutError;
 	CancelError: typeof errors.CancelError;
 
-	(url: URLArgument | Options & { stream?: false; url: URLArgument }, options?: Options & { stream?: false }): CancelableRequest<Response>;
-	(url: URLArgument | Options & { stream: true; url: URLArgument }, options?: Options & { stream: true }): ProxyStream;
-	(url: URLOrOptions, options?: Options): CancelableRequest<Response> | ProxyStream;
-	create(defaults: Defaults): Got;
 	extend(...instancesOrOptions: Array<Got | ExtendedOptions>): Got;
 	mergeInstances(parent: Got, ...instances: Got[]): Got;
-	mergeOptions<T extends Options>(...sources: T[]): T & { hooks: Partial<Hooks> };
+	mergeOptions<T extends Options>(...sources: T[]): T & {hooks: Partial<Hooks>};
 }
 
 export interface GotStream extends Record<HTTPAlias, ReturnStream> {
@@ -62,61 +83,43 @@ const aliases: readonly HTTPAlias[] = [
 	'delete'
 ];
 
-const defaultHandler: HandlerFunction = (options, next) => next(options);
+export const defaultHandler: HandlerFunction = (options, next) => next(options);
 
-// `got.mergeInstances()` is deprecated
-let hasShownDeprecation = false;
+const create = (defaults: Defaults): Got => {
+	// Proxy properties from next handlers
+	defaults._rawHandlers = defaults.handlers;
+	defaults.handlers = defaults.handlers.map(fn => ((options, next) => {
+		let root: GotReturn;
 
-const create = (nonNormalizedDefaults: Defaults): Got => {
-	const defaults: NormalizedDefaults = {
-		handlers: Reflect.has(nonNormalizedDefaults, 'handlers') ? merge([], nonNormalizedDefaults.handlers) : [defaultHandler],
-		options: preNormalizeArguments(mergeOptions(Reflect.has(nonNormalizedDefaults, 'options') ? nonNormalizedDefaults.options : {})),
-		mutableDefaults: Boolean(nonNormalizedDefaults.mutableDefaults)
-	};
+		const result = fn(options, newOptions => {
+			root = next(newOptions);
+			return root;
+		});
+
+		if (result !== root && !options.isStream) {
+			Object.setPrototypeOf(result, Object.getPrototypeOf(root));
+			Object.defineProperties(result, Object.getOwnPropertyDescriptors(root));
+		}
+
+		return result;
+	}) as HandlerFunction);
 
 	// @ts-ignore Because the for loop handles it for us, as well as the other Object.defines
 	const got: Got = (url: URLOrOptions, options?: Options): GotReturn => {
-		const isStream = options && options.stream;
-
 		let iteration = 0;
-		const iterateHandlers = (newOptions: NormalizedOptions): GotReturn => {
-			let nextPromise: CancelableRequest<Response>;
-			const result = defaults.handlers[iteration++](newOptions, options => {
-				const fn = iteration === defaults.handlers.length ? getPromiseOrStream : iterateHandlers;
-
-				if (isStream) {
-					return fn(options);
-				}
-
-				// We need to remember the `next(options)` result.
-				nextPromise = fn(options) as CancelableRequest<Response>;
-				return nextPromise;
-			});
-
-			// Proxy the properties from the next handler to this one
-			if (!isStream && !Reflect.has(result, isProxiedSymbol)) {
-				for (const key of Object.keys(nextPromise)) {
-					Object.defineProperty(result, key, {
-						get: () => {
-							return nextPromise[key];
-						},
-						set: (value: unknown) => {
-							nextPromise[key] = value;
-						}
-					});
-				}
-
-				(result as CancelableRequest<Response>).cancel = nextPromise.cancel;
-				result[isProxiedSymbol] = true;
-			}
-
-			return result;
+		const iterateHandlers: HandlerFunction = newOptions => {
+			return defaults.handlers[iteration++](
+				newOptions,
+				// @ts-ignore TS doesn't know that it calls `getPromiseOrStream` at the end
+				iteration === defaults.handlers.length ? getPromiseOrStream : iterateHandlers
+			);
 		};
 
 		try {
-			return iterateHandlers(normalizeArguments(url, options as NormalizedOptions, defaults));
+			// @ts-ignore This handler takes only one parameter.
+			return iterateHandlers(normalizeArguments(url, options, defaults));
 		} catch (error) {
-			if (isStream) {
+			if (options?.isStream) {
 				throw error;
 			} else {
 				// @ts-ignore It's an Error not a response, but TS thinks it's calling .resolve
@@ -125,20 +128,20 @@ const create = (nonNormalizedDefaults: Defaults): Got => {
 		}
 	};
 
-	got.create = create;
 	got.extend = (...instancesOrOptions) => {
-		const options: Options[] = [defaults.options];
-		const handlers: HandlerFunction[] = [...defaults.handlers];
+		const optionsArray: Options[] = [defaults.options];
+		let handlers: HandlerFunction[] = [...defaults._rawHandlers];
 		let mutableDefaults: boolean;
 
 		for (const value of instancesOrOptions) {
 			if (Reflect.has(value, 'defaults')) {
-				options.push((value as Got).defaults.options);
-				handlers.push(...(value as Got).defaults.handlers.filter(handler => handler !== defaultHandler));
+				optionsArray.push((value as Got).defaults.options);
+
+				handlers.push(...(value as Got).defaults._rawHandlers);
 
 				mutableDefaults = (value as Got).defaults.mutableDefaults;
 			} else {
-				options.push(value as Options);
+				optionsArray.push(value as ExtendedOptions);
 
 				if (Reflect.has(value, 'handlers')) {
 					handlers.push(...(value as ExtendedOptions).handlers);
@@ -148,29 +151,25 @@ const create = (nonNormalizedDefaults: Defaults): Got => {
 			}
 		}
 
-		handlers.push(defaultHandler);
+		handlers = handlers.filter(handler => handler !== defaultHandler);
+
+		if (handlers.length === 0) {
+			handlers.push(defaultHandler);
+		}
 
 		return create({
-			options: mergeOptions(...options),
+			options: mergeOptions(...optionsArray),
 			handlers,
 			mutableDefaults
 		});
 	};
 
-	got.mergeInstances = (parent, ...instances) => {
-		if (!hasShownDeprecation) {
-			console.warn('`got.mergeInstances()` is deprecated. We support it solely for compatibility - it will be removed in Got 11. Use `instance.extend(...instances)` instead.');
-			hasShownDeprecation = true;
-		}
-
-		return parent.extend(...instances);
-	};
-
 	// @ts-ignore The missing methods because the for-loop handles it for us
-	got.stream = (url, options) => got(url, {...options, stream: true});
+	got.stream = (url, options) => got(url, {...options, isStream: true});
 
 	for (const method of aliases) {
-		got[method] = (url, options) => got(url, {...options, method});
+		// @ts-ignore
+		got[method] = (url: URLOrOptions, options?: Options): GotReturn => got(url, {...options, method});
 		got.stream[method] = (url, options) => got.stream(url, {...options, method});
 	}
 
diff --git a/source/errors.ts b/source/errors.ts
index 483ecda..b880772 100644
--- a/source/errors.ts
+++ b/source/errors.ts
@@ -1,4 +1,3 @@
-import {format} from 'url';
 import is from '@sindresorhus/is';
 import {Timings} from '@szmarczak/http-timer';
 import {Response, NormalizedOptions} from './utils/types';
@@ -6,8 +5,7 @@ import {TimeoutError as TimedOutError} from './utils/timed-out';
 
 export class GotError extends Error {
 	code?: string;
-
-	readonly options!: NormalizedOptions;
+	readonly options: NormalizedOptions;
 
 	constructor(message: string, error: (Error & {code?: string}) | {code?: string}, options: NormalizedOptions) {
 		super(message);
@@ -46,10 +44,10 @@ export class ReadError extends GotError {
 }
 
 export class ParseError extends GotError {
-	readonly response!: Response;
+	readonly response: Response;
 
 	constructor(error: Error, response: Response, options: NormalizedOptions) {
-		super(`${error.message} in "${format(options as unknown as URL)}"`, error, options);
+		super(`${error.message} in "${options.url.toString()}"`, error, options);
 		this.name = 'ParseError';
 
 		Object.defineProperty(this, 'response', {
@@ -59,7 +57,7 @@ export class ParseError extends GotError {
 }
 
 export class HTTPError extends GotError {
-	readonly response!: Response;
+	readonly response: Response;
 
 	constructor(response: Response, options: NormalizedOptions) {
 		const {statusCode, statusMessage} = response;
@@ -74,7 +72,7 @@ export class HTTPError extends GotError {
 }
 
 export class MaxRedirectsError extends GotError {
-	readonly response!: Response;
+	readonly response: Response;
 
 	constructor(response: Response, maxRedirects: number, options: NormalizedOptions) {
 		super(`Redirected ${maxRedirects} times. Aborting.`, {}, options);
@@ -88,14 +86,13 @@ export class MaxRedirectsError extends GotError {
 
 export class UnsupportedProtocolError extends GotError {
 	constructor(options: NormalizedOptions) {
-		super(`Unsupported protocol "${options.protocol}"`, {}, options);
+		super(`Unsupported protocol "${options.url.protocol}"`, {}, options);
 		this.name = 'UnsupportedProtocolError';
 	}
 }
 
 export class TimeoutError extends GotError {
 	timings: Timings;
-
 	event: string;
 
 	constructor(error: TimedOutError, timings: Timings, options: NormalizedOptions) {
diff --git a/source/get-response.ts b/source/get-response.ts
index 3c0ef51..91b3001 100644
--- a/source/get-response.ts
+++ b/source/get-response.ts
@@ -1,7 +1,6 @@
 import {IncomingMessage} from 'http';
 import EventEmitter = require('events');
 import stream = require('stream');
-import is from '@sindresorhus/is';
 import decompressResponse = require('decompress-response');
 import mimicResponse = require('mimic-response');
 import {NormalizedOptions, Response} from './utils/types';
@@ -9,17 +8,16 @@ import {downloadProgress} from './progress';
 
 export default (response: IncomingMessage, options: NormalizedOptions, emitter: EventEmitter) => {
 	const downloadBodySize = Number(response.headers['content-length']) || undefined;
-	const progressStream = downloadProgress(response, emitter, downloadBodySize);
+	const progressStream = downloadProgress(emitter, downloadBodySize);
 
 	mimicResponse(response, progressStream);
 
 	const newResponse = (
-		options.decompress === true &&
-		is.function_(decompressResponse) &&
+		options.decompress &&
 		options.method !== 'HEAD' ? decompressResponse(progressStream as unknown as IncomingMessage) : progressStream
 	) as Response;
 
-	if (!options.decompress && ['gzip', 'deflate', 'br'].includes(response.headers['content-encoding'] || '')) {
+	if (!options.decompress && ['gzip', 'deflate', 'br'].includes(response.headers['content-encoding'] ?? '')) {
 		options.encoding = null;
 	}
 
diff --git a/source/index.ts b/source/index.ts
index 39a6171..21bee3f 100644
--- a/source/index.ts
+++ b/source/index.ts
@@ -1,7 +1,7 @@
-import create from './create';
+import create, {defaultHandler} from './create';
 import {Defaults} from './utils/types.js';
 
-const defaults: Partial<Defaults> = {
+const defaults: Defaults = {
 	options: {
 		method: 'GET',
 		retry: {
@@ -32,8 +32,11 @@ const defaults: Partial<Defaults> = {
 				'ENOTFOUND',
 				'ENETUNREACH',
 				'EAI_AGAIN'
-			]
+			],
+			maxRetryAfter: undefined,
+			calculateDelay: ({computedValue}) => computedValue
 		},
+		timeout: {},
 		headers: {
 			'user-agent': 'got (https://github.com/sindresorhus/got)'
 		},
@@ -46,14 +49,16 @@ const defaults: Partial<Defaults> = {
 		decompress: true,
 		throwHttpErrors: true,
 		followRedirect: true,
-		stream: false,
+		isStream: false,
 		cache: false,
 		dnsCache: false,
 		useElectronNet: false,
-		responseType: 'text',
+		responseType: 'default',
 		resolveBodyOnly: false,
-		maxRedirects: 10
+		maxRedirects: 10,
+		prefixUrl: ''
 	},
+	handlers: [defaultHandler],
 	mutableDefaults: false
 };
 
@@ -71,7 +76,6 @@ export * from './utils/types';
 export {
 	Got,
 	GotStream,
-	ReturnResponse,
 	ReturnStream,
 	GotReturn
 } from './create';
diff --git a/source/known-hook-events.ts b/source/known-hook-events.ts
index d3591cc..9baa49f 100644
--- a/source/known-hook-events.ts
+++ b/source/known-hook-events.ts
@@ -1,5 +1,4 @@
-import {Options, CancelableRequest, Response, NormalizedOptions} from './utils/types';
-import {HTTPError, GotError, ParseError, MaxRedirectsError} from './errors';
+import {CancelableRequest, Response, NormalizedOptions, GenericError} from './utils/types';
 
 /**
 Called with plain request options, right before their normalization. This is especially useful in conjunction with got.extend() and got.create() when the input needs custom handling.
@@ -8,7 +7,7 @@ Called with plain request options, right before their normalization. This is esp
 
 @see [Request migration guide](https://github.com/sindresorhus/got/blob/master/migration-guides.md#breaking-changes) for an example.
 */
-export type InitHook = (options: Options) => void;
+export type InitHook = (options: NormalizedOptions) => void;
 
 /**
 Called with normalized [request options](https://github.com/sindresorhus/got#options). Got will make no further changes to the request before it is sent (except the body serialization). This is especially useful in conjunction with [`got.extend()`](https://github.com/sindresorhus/got#instances) and [`got.create()`](https://github.com/sindresorhus/got/blob/master/advanced-creation.md) when you want to create an API client that, for example, uses HMAC-signing.
@@ -25,14 +24,14 @@ export type BeforeRedirectHook = (options: NormalizedOptions, response: Response
 /**
 Called with normalized [request options](https://github.com/sindresorhus/got#options), the error and the retry count. Got will make no further changes to the request. This is especially useful when some extra work is required before the next try.
 */
-export type BeforeRetryHook = (options: NormalizedOptions, error: Error | GotError | ParseError | HTTPError | MaxRedirectsError, retryCount: number) => void | Promise<void>;
+export type BeforeRetryHook = (options: NormalizedOptions, error?: GenericError, retryCount?: number) => void | Promise<void>;
 
 /**
 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.
 */
-export type BeforeErrorHook = <ErrorLike extends Error | GotError | ParseError | HTTPError | MaxRedirectsError>(error: ErrorLike) => Error | Promise<Error>;
+export type BeforeErrorHook = <ErrorLike extends GenericError>(error: ErrorLike) => Error | Promise<Error>;
 
 /**
 Called with [response object](https://github.com/sindresorhus/got#response) and a retry function.
diff --git a/source/merge.ts b/source/merge.ts
deleted file mode 100644
index 048bf65..0000000
--- a/source/merge.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import is from '@sindresorhus/is';
-import {Options} from './utils/types';
-import knownHookEvents, {Hooks, HookEvent, HookType} from './known-hook-events';
-
-export default function merge<Target extends Record<string, any>, Source extends Record<string, any>>(target: Target, ...sources: Source[]): Target & Source {
-	for (const source of sources) {
-		for (const [key, sourceValue] of Object.entries(source)) {
-			if (is.undefined(sourceValue)) {
-				continue;
-			}
-
-			const targetValue = target[key];
-			if (targetValue instanceof URLSearchParams && sourceValue instanceof URLSearchParams) {
-				const params = new URLSearchParams();
-
-				const append = (value: string, key: string): void => params.append(key, value);
-				targetValue.forEach(append);
-				sourceValue.forEach(append);
-
-				// @ts-ignore https://github.com/microsoft/TypeScript/issues/31661
-				target[key] = params;
-			} else if (is.urlInstance(targetValue) && (is.urlInstance(sourceValue) || is.string(sourceValue))) {
-				// @ts-ignore
-				target[key] = new URL(sourceValue as string, targetValue);
-			} else if (is.plainObject(sourceValue)) {
-				if (is.plainObject(targetValue)) {
-					// @ts-ignore
-					target[key] = merge({}, targetValue, sourceValue);
-				} else {
-					// @ts-ignore
-					target[key] = merge({}, sourceValue);
-				}
-			} else if (is.array(sourceValue)) {
-				// @ts-ignore
-				target[key] = sourceValue.slice();
-			} else {
-				// @ts-ignore
-				target[key] = sourceValue;
-			}
-		}
-	}
-
-	return target as Target & Source;
-}
-
-export function mergeOptions(...sources: Array<Partial<Options>>): Partial<Options> {
-	sources = sources.map(source => {
-		if (!source) {
-			return {};
-		}
-
-		if (is.object(source.retry)) {
-			return source;
-		}
-
-		return {
-			...source,
-			retry: {
-				retries: source.retry
-			}
-		};
-	}) as Array<Partial<Options>>;
-
-	const mergedOptions = merge({}, ...sources);
-
-	const hooks = knownHookEvents.reduce((accumulator, current) => ({...accumulator, [current]: []}), {}) as Record<HookEvent, HookType[]>;
-
-	for (const source of sources) {
-		// We need to check `source` to allow calling `.extend()` with no arguments.
-		if (source) {
-			if (Reflect.has(source, 'hooks')) {
-				for (const hook of knownHookEvents) {
-					hooks[hook] = hooks[hook].concat(source.hooks[hook] || []);
-				}
-			}
-
-			if (Reflect.has(source, 'context')) {
-				Object.defineProperty(mergedOptions, 'context', {
-					writable: true,
-					configurable: true,
-					enumerable: false,
-					// @ts-ignore
-					value: source.context
-				});
-			}
-
-			if (Reflect.has(source, 'body')) {
-				mergedOptions.body = source.body;
-			}
-
-			if (Reflect.has(source, 'json')) {
-				mergedOptions.json = source.json;
-			}
-
-			if (Reflect.has(source, 'form')) {
-				mergedOptions.form = source.form;
-			}
-		}
-	}
-
-	mergedOptions.hooks = hooks as Hooks;
-
-	return mergedOptions;
-}
diff --git a/source/normalize-arguments.ts b/source/normalize-arguments.ts
index c452f55..b6be324 100644
--- a/source/normalize-arguments.ts
+++ b/source/normalize-arguments.ts
@@ -1,57 +1,65 @@
+import {promisify} from 'util';
+import http = require('http');
 import https = require('https');
-import {format} from 'url';
 import CacheableLookup from 'cacheable-lookup';
+import CacheableRequest = require('cacheable-request');
 import is from '@sindresorhus/is';
 import lowercaseKeys = require('lowercase-keys');
+import toReadableStream = require('to-readable-stream');
 import Keyv = require('keyv');
-import urlToOptions, {URLOptions} from './utils/url-to-options';
-import validateSearchParams from './utils/validate-search-params';
-import supportsBrotli from './utils/supports-brotli';
-import merge, {mergeOptions} from './merge';
+import optionsToUrl from './utils/options-to-url';
+import {UnsupportedProtocolError} from './errors';
+import merge from './utils/merge';
 import knownHookEvents from './known-hook-events';
 import {
 	Options,
 	NormalizedOptions,
 	Method,
-	URLArgument,
 	URLOrOptions,
-	NormalizedDefaults
+	Defaults
 } from './utils/types';
+import dynamicRequire from './utils/dynamic-require';
+import getBodySize from './utils/get-body-size';
+import isFormData from './utils/is-form-data';
+import supportsBrotli from './utils/supports-brotli';
 
-let hasShownDeprecation = false;
-
-// It's 2x faster than [...new Set(array)]
-const uniqueArray = <T>(array: T[]): T[] => array.filter((element, position) => array.indexOf(element) === position);
+// `preNormalizeArguments` normalizes these options: `headers`, `prefixUrl`, `hooks`, `timeout`, `retry` and `method`.
+// `normalizeArguments` is *only* called on `got(...)`. It normalizes the URL and performs `mergeOptions(...)`.
+// `normalizeRequestArguments` converts Got options into HTTP options.
 
-// `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, `url` needs to be normalized every request.
+type NonEnumerableProperty = 'context' | 'body' | 'json' | 'form';
+const nonEnumerableProperties: NonEnumerableProperty[] = [
+	'context',
+	'body',
+	'json',
+	'form'
+];
 
-// TODO: document this.
 export const preNormalizeArguments = (options: Options, defaults?: NormalizedOptions): NormalizedOptions => {
 	// `options.headers`
-	if (is.nullOrUndefined(options.headers)) {
+	if (is.undefined(options.headers)) {
 		options.headers = {};
 	} else {
 		options.headers = lowercaseKeys(options.headers);
 	}
 
 	// `options.prefixUrl`
-	if (options.prefixUrl) {
+	if (is.urlInstance(options.prefixUrl) || is.string(options.prefixUrl)) {
 		options.prefixUrl = options.prefixUrl.toString();
 
-		if (!options.prefixUrl.endsWith('/')) {
+		if (options.prefixUrl.length !== 0 && !options.prefixUrl.endsWith('/')) {
 			options.prefixUrl += '/';
 		}
+	} else {
+		options.prefixUrl = defaults ? defaults.prefixUrl : '';
 	}
 
 	// `options.hooks`
-	if (is.nullOrUndefined(options.hooks)) {
+	if (is.undefined(options.hooks)) {
 		options.hooks = {};
-	} else if (is.object(options.hooks)) {
+	}
+
+	if (is.object(options.hooks)) {
 		for (const event of knownHookEvents) {
 			if (Reflect.has(options.hooks, event)) {
 				if (!is.array(options.hooks[event])) {
@@ -65,6 +73,16 @@ export const preNormalizeArguments = (options: Options, defaults?: NormalizedOpt
 		throw new TypeError(`Parameter \`hooks\` must be an Object, not ${is(options.hooks)}`);
 	}
 
+	if (defaults) {
+		for (const event of knownHookEvents) {
+			// @ts-ignore TS is dumb.
+			options.hooks[event] = [
+				...defaults.hooks[event],
+				...options.hooks[event]
+			];
+		}
+	}
+
 	// `options.timeout`
 	if (is.number(options.timeout)) {
 		options.timeout = {request: options.timeout};
@@ -75,7 +93,7 @@ export const preNormalizeArguments = (options: Options, defaults?: NormalizedOpt
 	// `options.retry`
 	const {retry} = options;
 
-	if (defaults && Reflect.has(defaults, 'retry')) {
+	if (defaults) {
 		options.retry = {...defaults.retry};
 	} else {
 		options.retry = {
@@ -103,142 +121,309 @@ export const preNormalizeArguments = (options: Options, defaults?: NormalizedOpt
 		);
 	}
 
-	options.retry.methods = uniqueArray(options.retry.methods.map(method => method.toUpperCase())) as Method[];
-	options.retry.statusCodes = uniqueArray(options.retry.statusCodes);
-	options.retry.errorCodes = uniqueArray(options.retry.errorCodes);
+	options.retry.methods = [...new Set(options.retry.methods.map(method => method.toUpperCase()))] as Method[];
+	options.retry.statusCodes = [...new Set(options.retry.statusCodes)];
+	options.retry.errorCodes = [...new Set(options.retry.errorCodes)];
 
 	// `options.dnsCache`
-	if (options.dnsCache && !(options instanceof CacheableLookup)) {
-		options.dnsCache = new CacheableLookup({cacheAdapter: options.dnsCache as Keyv | undefined});
+	if (options.dnsCache && !(options.dnsCache instanceof CacheableLookup)) {
+		options.dnsCache = new CacheableLookup({cacheAdapter: options.dnsCache as Keyv});
 	}
 
-	return options as NormalizedOptions;
-};
-
-export const normalizeArguments = (url: URLOrOptions, options: Options, defaults?: NormalizedDefaults): NormalizedOptions => {
-	let urlArgument: URLArgument;
-	if (is.plainObject(url)) {
-		options = {...url as Options, ...options};
-		urlArgument = options.url || {};
-		delete options.url;
+	// `options.method`
+	if (is.string(options.method)) {
+		options.method = options.method.toUpperCase() as Method;
 	} else {
-		urlArgument = url;
+		options.method = defaults?.method || 'GET';
 	}
 
-	if (defaults) {
-		options = mergeOptions(defaults.options, options ? preNormalizeArguments(options, defaults.options) : {});
-	} else {
-		options = merge({}, preNormalizeArguments(options));
+	// Better memory management, so we don't have to generate a new object every time
+	if (options.cache) {
+		(options as NormalizedOptions).cacheableRequest = new CacheableRequest(
+			(options, handler) => options.request(options, handler),
+			options.cache as any
+		);
 	}
 
-	if (!is.string(urlArgument) && !is.object(urlArgument)) {
-		throw new TypeError(`Parameter \`url\` must be a string or an Object, not ${is(urlArgument)}`);
+	// `options.cookieJar`
+	if (is.object(options.cookieJar)) {
+		let {setCookie, getCookieString} = options.cookieJar;
+
+		// Horrible `tough-cookie` check
+		if (setCookie.length === 4 && getCookieString.length === 0) {
+			if (!Reflect.has(setCookie, promisify.custom)) {
+				setCookie = promisify(setCookie.bind(options.cookieJar));
+				getCookieString = promisify(getCookieString.bind(options.cookieJar));
+			}
+		} else if (setCookie.length !== 2) {
+			throw new TypeError('`options.cookieJar.setCookie` needs to be an async function with 2 arguments');
+		} else if (getCookieString.length !== 1) {
+			throw new TypeError('`options.cookieJar.getCookieString` needs to be an async function with 1 argument');
+		}
+
+		options.cookieJar = {setCookie, getCookieString};
 	}
 
-	let urlObj: https.RequestOptions | URLOptions;
-	if (is.string(urlArgument)) {
-		if (options.prefixUrl && urlArgument.startsWith('/')) {
-			throw new Error('`url` must not begin with a slash when using `prefixUrl`');
+	return options as NormalizedOptions;
+};
+
+export const mergeOptions = (...sources: Options[]): NormalizedOptions => {
+	const mergedOptions = preNormalizeArguments({});
+
+	// Non enumerable properties shall not be merged
+	const properties = {};
+
+	for (const source of sources) {
+		if (!source) {
+			continue;
 		}
 
-		if (options.prefixUrl) {
-			urlArgument = options.prefixUrl.toString() + urlArgument;
+		merge(mergedOptions, preNormalizeArguments(merge({}, source), mergedOptions));
+
+		for (const name of nonEnumerableProperties) {
+			if (!Reflect.has(source, name)) {
+				continue;
+			}
+
+			properties[name] = {
+				writable: true,
+				configurable: true,
+				enumerable: false,
+				value: source[name]
+			};
 		}
+	}
 
-		urlArgument = urlArgument.replace(/^unix:/, 'http://$&');
+	Object.defineProperties(mergedOptions, properties);
 
-		urlObj = urlToOptions(new URL(urlArgument));
-	} else if (is.urlInstance(urlArgument)) {
-		urlObj = urlToOptions(urlArgument);
-	} else if (options.prefixUrl) {
-		urlObj = {
-			// @ts-ignore
-			...urlToOptions(new URL(options.prefixUrl)),
-			...urlArgument
-		};
-	} else {
-		urlObj = urlArgument;
+	return mergedOptions;
+};
+
+export const normalizeArguments = (url: URLOrOptions, options?: Options, defaults?: Defaults): NormalizedOptions => {
+	// Merge options
+	if (typeof url === 'undefined') {
+		throw new TypeError('Missing `url` argument');
 	}
 
-	if (!Reflect.has(urlObj, 'protocol') && !Reflect.has(options, 'protocol')) {
-		throw new TypeError('No URL protocol specified');
+	if (typeof options === 'undefined') {
+		options = {};
 	}
 
-	options = mergeOptions(urlObj, options);
+	if (is.urlInstance(url) || is.string(url)) {
+		options.url = url;
 
-	for (const hook of options.hooks.init) {
-		if (is.asyncFunction(hook)) {
-			throw new TypeError('The `init` hook must be a synchronous function');
+		options = mergeOptions(defaults && defaults.options, options);
+	} else {
+		if (Reflect.has(url, 'resolve')) {
+			throw new Error('The legacy `url.Url` is deprecated. Use `URL` instead.');
 		}
 
-		// @ts-ignore TS is dumb.
-		hook(options);
+		options = mergeOptions(defaults && defaults.options, url, options);
 	}
 
-	const {prefixUrl} = options;
+	// Normalize URL
+	// TODO: drop `optionsToUrl` in Got 12
+	if (is.string(options.url)) {
+		options.url = (options.prefixUrl as string) + options.url;
+		options.url = options.url.replace(/^unix:/, 'http://$&');
+
+		if (options.searchParams || options.search) {
+			options.url = options.url.split('?')[0];
+		}
+
+		options.url = optionsToUrl({
+			origin: options.url,
+			...options
+		});
+	} else if (!is.urlInstance(options.url)) {
+		options.url = optionsToUrl({origin: options.prefixUrl as string, ...options});
+	}
+
+	const normalizedOptions = options as NormalizedOptions;
+
+	// Make it possible to change `options.prefixUrl`
+	let prefixUrl = options.prefixUrl as string;
 	Object.defineProperty(options, 'prefixUrl', {
-		set: () => {
-			throw new Error('Failed to set prefixUrl. Options are normalized already.');
+		set: (value: string) => {
+			if (!normalizedOptions.url.href.startsWith(value)) {
+				throw new Error(`Cannot change \`prefixUrl\` from ${prefixUrl} to ${value}: ${normalizedOptions.url.href}`);
+			}
+
+			normalizedOptions.url = new URL(value + normalizedOptions.url.href.slice(prefixUrl.length));
+			prefixUrl = value;
 		},
 		get: () => prefixUrl
 	});
 
-	let {searchParams} = options;
-	delete options.searchParams;
+	// Make it possible to remove default headers
+	for (const [key, value] of Object.entries(options.headers)) {
+		if (is.undefined(value)) {
+			delete options.headers[key];
+		} else if (is.null_(value)) {
+			throw new TypeError('Use `undefined` instead of `null` to delete HTTP headers');
+		}
+	}
 
-	// TODO: Remove this before Got v11
-	if (options.query) {
-		if (!hasShownDeprecation) {
-			console.warn('`options.query` is deprecated. We support it solely for compatibility - it will be removed in Got 11. Use `options.searchParams` instead.');
-			hasShownDeprecation = true;
+	for (const hook of options.hooks.init) {
+		if (is.asyncFunction(hook)) {
+			throw new TypeError('The `init` hook must be a synchronous function');
 		}
 
-		searchParams = options.query;
-		delete options.query;
+		// @ts-ignore TS is dumb.
+		hook(normalizedOptions);
+	}
+
+	return normalizedOptions;
+};
+
+const withoutBody: ReadonlySet<string> = new Set(['GET', 'HEAD']);
+
+export type NormalizedRequestArguments = https.RequestOptions & {
+	body: Pick<NormalizedOptions, 'body'>;
+	url: Pick<NormalizedOptions, 'url'>;
+};
+
+export const normalizeRequestArguments = async (options: NormalizedOptions): Promise<NormalizedRequestArguments> => {
+	options = mergeOptions(options);
+
+	let uploadBodySize: number | undefined;
+
+	// Serialize body
+	const {headers} = options;
+	const isForm = !is.undefined(options.form);
+	const isJSON = !is.undefined(options.json);
+	const isBody = !is.undefined(options.body);
+	if ((isBody || isForm || isJSON) && withoutBody.has(options.method)) {
+		throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
+	}
+
+	if ([isBody, isForm, isJSON].filter(isTrue => isTrue).length > 1) {
+		throw new TypeError('The `body`, `json` and `form` options are mutually exclusive');
 	}
 
-	if (is.nonEmptyString(searchParams) || is.nonEmptyObject(searchParams) || (searchParams && searchParams instanceof URLSearchParams)) {
-		if (!is.string(searchParams)) {
-			if (!(searchParams instanceof URLSearchParams)) {
-				validateSearchParams(searchParams);
+	if (isBody) {
+		if (is.object(options.body) && isFormData(options.body)) {
+			// Special case for https://github.com/form-data/form-data
+			if (!Reflect.has(headers, 'content-type')) {
+				headers['content-type'] = `multipart/form-data; boundary=${options.body.getBoundary()}`;
 			}
+		} else if (!is.nodeStream(options.body) && !is.string(options.body) && !is.buffer(options.body)) {
+			throw new TypeError('The `body` option must be a stream.Readable, string or Buffer');
+		}
+	} else if (isForm) {
+		if (!is.object(options.form)) {
+			throw new TypeError('The `form` option must be an Object');
+		}
+
+		if (!Reflect.has(headers, 'content-type')) {
+			headers['content-type'] = 'application/x-www-form-urlencoded';
+		}
+
+		options.body = (new URLSearchParams(options.form as Record<string, string>)).toString();
+	} else if (isJSON) {
+		if (!Reflect.has(headers, 'content-type')) {
+			headers['content-type'] = 'application/json';
+		}
+
+		options.body = JSON.stringify(options.json);
+	}
+
+	// Convert buffer to stream to receive upload progress events (#322)
+	if (is.buffer(options.body)) {
+		uploadBodySize = options.body.length;
+		options.body = toReadableStream(options.body);
+	} else {
+		uploadBodySize = await getBodySize(options);
+	}
 
-			searchParams = (new URLSearchParams(searchParams as Record<string, string>)).toString();
+	// See https://tools.ietf.org/html/rfc7230#section-3.3.2
+	// A user agent SHOULD send a Content-Length in a request message when
+	// no Transfer-Encoding is sent and the request method defines a meaning
+	// for an enclosed payload body.  For example, a Content-Length header
+	// field is normally sent in a POST request even when the value is 0
+	// (indicating an empty payload body).  A user agent SHOULD NOT send a
+	// Content-Length header field when the request message does not contain
+	// a payload body and the method semantics do not anticipate such a
+	// body.
+	if (!Reflect.has(headers, 'content-length') && !Reflect.has(headers, 'transfer-encoding')) {
+		if (
+			(options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH') &&
+			!is.undefined(uploadBodySize)
+		) {
+			headers['content-length'] = String(uploadBodySize);
 		}
+	}
 
-		options.path = `${options.path.split('?')[0]}?${searchParams}`;
+	if (!options.isStream && options.responseType === 'json' && is.undefined(headers.accept)) {
+		headers.accept = 'application/json';
 	}
 
-	if (options.hostname === 'unix') {
-		const matches = /(?<socketPath>.+?):(?<path>.+)/.exec(options.path);
+	if (options.decompress && is.undefined(headers['accept-encoding'])) {
+		headers['accept-encoding'] = supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate';
+	}
+
+	// Validate URL
+	if (options.url.protocol !== 'http:' && options.url.protocol !== 'https:') {
+		throw new UnsupportedProtocolError(options);
+	}
 
-		if (matches) {
+	decodeURI(options.url.toString());
+
+	// Normalize request function
+	if (!is.function_(options.request)) {
+		options.request = options.url.protocol === 'https:' ? https.request : http.request;
+	}
+
+	// UNIX sockets
+	if (options.url.hostname === 'unix') {
+		const matches = /(?<socketPath>.+?):(?<path>.+)/.exec(options.url.pathname);
+
+		if (matches?.groups) {
 			const {socketPath, path} = matches.groups;
+
+			// It's a bug!
+			// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
 			options = {
 				...options,
 				socketPath,
 				path,
 				host: ''
-			};
+			} as NormalizedOptions;
 		}
 	}
 
-	const {headers} = options;
-	for (const [key, value] of Object.entries(headers)) {
-		if (is.nullOrUndefined(value)) {
-			delete headers[key];
-		}
+	if (is.object(options.agent)) {
+		options.agent = options.agent[options.url.protocol.slice(0, -1)] || options.agent;
 	}
 
-	if (options.decompress && is.undefined(headers['accept-encoding'])) {
-		headers['accept-encoding'] = supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate';
+	if (options.dnsCache) {
+		options.lookup = options.dnsCache.lookup;
 	}
 
-	if (options.method) {
-		options.method = options.method.toUpperCase() as Method;
+	/* istanbul ignore next: electron.net is broken */
+	// No point in typing process.versions correctly, as
+	// `process.version.electron` is used only once, right here.
+	if (options.useElectronNet && (process.versions as any).electron) {
+		const electron = dynamicRequire(module, 'electron') as any; // Trick webpack
+		options.request = electron.net.request ?? electron.remote.net.request;
 	}
 
-	return options as NormalizedOptions;
-};
+	// Got's `timeout` is an object, http's `timeout` is a number, so they're not compatible.
+	delete options.timeout;
 
-export const reNormalizeArguments = (options: Options): NormalizedOptions => normalizeArguments(format(options as unknown as URL | URLOptions), options);
+	// Set cookies
+	if (options.cookieJar) {
+		const cookieString = await options.cookieJar.getCookieString(options.url.toString());
+
+		if (is.nonEmptyString(cookieString)) {
+			options.headers.cookie = cookieString;
+		} else {
+			delete options.headers.cookie;
+		}
+	}
+
+	// `http-cache-semantics` checks this
+	delete options.url;
+
+	return options as unknown as NormalizedRequestArguments;
+};
diff --git a/source/progress.ts b/source/progress.ts
index 08b6f70..806b6ec 100644
--- a/source/progress.ts
+++ b/source/progress.ts
@@ -1,22 +1,23 @@
-import {IncomingMessage, ClientRequest} from 'http';
+import {ClientRequest} from 'http';
 import {Transform as TransformStream} from 'stream';
 import {Socket} from 'net';
 import EventEmitter = require('events');
+import is from '@sindresorhus/is';
 
-export function downloadProgress(_response: IncomingMessage, emitter: EventEmitter, downloadBodySize?: number): TransformStream {
-	let downloaded = 0;
+export function downloadProgress(emitter: EventEmitter, downloadBodySize?: number): TransformStream {
+	let downloadedBytes = 0;
 
-	return new TransformStream({
+	const progressStream = new TransformStream({
 		transform(chunk, _encoding, callback) {
-			downloaded += chunk.length;
+			downloadedBytes += chunk.length;
 
-			const percent = downloadBodySize ? downloaded / downloadBodySize : 0;
+			const percent = downloadBodySize ? downloadedBytes / downloadBodySize : 0;
 
 			// Let `flush()` be responsible for emitting the last event
 			if (percent < 1) {
 				emitter.emit('downloadProgress', {
 					percent,
-					transferred: downloaded,
+					transferred: downloadedBytes,
 					total: downloadBodySize
 				});
 			}
@@ -27,18 +28,20 @@ export function downloadProgress(_response: IncomingMessage, emitter: EventEmitt
 		flush(callback) {
 			emitter.emit('downloadProgress', {
 				percent: 1,
-				transferred: downloaded,
+				transferred: downloadedBytes,
 				total: downloadBodySize
 			});
 
 			callback();
 		}
 	});
+
+	return progressStream;
 }
 
 export function uploadProgress(request: ClientRequest, emitter: EventEmitter, uploadBodySize?: number): void {
 	const uploadEventFrequency = 150;
-	let uploaded = 0;
+	let uploadedBytes = 0;
 	let progressInterval: NodeJS.Timeout;
 
 	emitter.emit('uploadProgress', {
@@ -51,12 +54,16 @@ export function uploadProgress(request: ClientRequest, emitter: EventEmitter, up
 		clearInterval(progressInterval);
 	});
 
+	request.once('abort', () => {
+		clearInterval(progressInterval);
+	});
+
 	request.once('response', () => {
 		clearInterval(progressInterval);
 
 		emitter.emit('uploadProgress', {
 			percent: 1,
-			transferred: uploaded,
+			transferred: uploadedBytes,
 			total: uploadBodySize
 		});
 	});
@@ -64,21 +71,33 @@ export function uploadProgress(request: ClientRequest, emitter: EventEmitter, up
 	request.once('socket', (socket: Socket) => {
 		const onSocketConnect = (): void => {
 			progressInterval = setInterval(() => {
-				const lastUploaded = uploaded;
-				/* istanbul ignore next: see #490 (occurs randomly!) */
-				const headersSize = (request as any)._header ? Buffer.byteLength((request as any)._header) : 0;
-				uploaded = socket.bytesWritten - headersSize;
+				const lastUploadedBytes = uploadedBytes;
+
+				/* istanbul ignore next: future versions of Node may not have this property */
+				if (!is.string((request as any)._header)) {
+					clearInterval(progressInterval);
+
+					const url = new URL('https://github.com/sindresorhus/got/issues/new');
+					url.searchParams.set('title', '`request._header` is not present');
+					url.searchParams.set('body', 'It causes `uploadProgress` to fail.');
+
+					console.warn('`request._header` is not present. Please report this as a bug:\n' + url.href);
+					return;
+				}
+
+				const headersSize = Buffer.byteLength((request as any)._header);
+				uploadedBytes = socket.bytesWritten - headersSize;
 
 				// Don't emit events with unchanged progress and
 				// prevent last event from being emitted, because
 				// it's emitted when `response` is emitted
-				if (uploaded === lastUploaded || uploaded === uploadBodySize) {
+				if (uploadedBytes === lastUploadedBytes || uploadedBytes === uploadBodySize) {
 					return;
 				}
 
 				emitter.emit('uploadProgress', {
-					percent: uploadBodySize ? uploaded / uploadBodySize : 0,
-					transferred: uploaded,
+					percent: uploadBodySize ? uploadedBytes / uploadBodySize : 0,
+					transferred: uploadedBytes,
 					total: uploadBodySize
 				});
 			}, uploadEventFrequency);
diff --git a/source/request-as-event-emitter.ts b/source/request-as-event-emitter.ts
index 2c07ef7..4864092 100644
--- a/source/request-as-event-emitter.ts
+++ b/source/request-as-event-emitter.ts
@@ -1,46 +1,34 @@
-import {format, UrlObject} from 'url';
-import {promisify} from 'util';
 import stream = require('stream');
 import EventEmitter = require('events');
-import {Transform as TransformStream} from 'stream';
 import http = require('http');
-import https = require('https');
 import CacheableRequest = require('cacheable-request');
-import toReadableStream = require('to-readable-stream');
 import is from '@sindresorhus/is';
 import timer, {Timings} from '@szmarczak/http-timer';
 import timedOut, {TimeoutError as TimedOutTimeoutError} from './utils/timed-out';
-import getBodySize from './utils/get-body-size';
-import isFormData from './utils/is-form-data';
 import calculateRetryDelay from './calculate-retry-delay';
 import getResponse from './get-response';
+import {normalizeRequestArguments} from './normalize-arguments';
 import {uploadProgress} from './progress';
-import {CacheError, UnsupportedProtocolError, MaxRedirectsError, RequestError, TimeoutError} from './errors';
+import {CacheError, MaxRedirectsError, RequestError, TimeoutError} from './errors';
 import urlToOptions from './utils/url-to-options';
-import {RequestFunction, NormalizedOptions, Response, ResponseObject, AgentByProtocol} from './utils/types';
-import dynamicRequire from './utils/dynamic-require';
+import {NormalizedOptions, Response, ResponseObject} from './utils/types';
 
 const redirectCodes: ReadonlySet<number> = new Set([300, 301, 302, 303, 304, 307, 308]);
-const withoutBody: ReadonlySet<string> = new Set(['GET', 'HEAD']);
 
 export interface RequestAsEventEmitter extends EventEmitter {
 	retry: <T extends Error>(error: T) => boolean;
 	abort: () => void;
 }
 
-export default (options: NormalizedOptions, input?: TransformStream) => {
+export default (options: NormalizedOptions) => {
 	const emitter = new EventEmitter() as RequestAsEventEmitter;
-	const redirects = [] as string[];
-	let currentRequest: http.ClientRequest;
-	let requestUrl: string;
-	let redirectString: string;
-	let uploadBodySize: number | undefined;
+
+	const requestURL = options.url.toString();
+	const redirects: string[] = [];
 	let retryCount = 0;
-	let shouldAbort = false;
 
-	const setCookie = options.cookieJar ? promisify(options.cookieJar.setCookie.bind(options.cookieJar)) : null;
-	const getCookieString = options.cookieJar ? promisify(options.cookieJar.getCookieString.bind(options.cookieJar)) : null;
-	const agents = is.object(options.agent) ? options.agent : null;
+	let currentRequest: http.ClientRequest;
+	let shouldAbort = false;
 
 	const emitError = async (error: Error): Promise<void> => {
 		try {
@@ -55,47 +43,11 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
 		}
 	};
 
-	const get = async (options: NormalizedOptions): Promise<void> => {
-		const currentUrl = redirectString || requestUrl;
-
-		if (options.protocol !== 'http:' && options.protocol !== 'https:') {
-			throw new UnsupportedProtocolError(options);
-		}
-
-		decodeURI(currentUrl);
-
-		let requestFn: RequestFunction;
-		if (is.function_(options.request)) {
-			requestFn = options.request;
-		} else {
-			requestFn = options.protocol === 'https:' ? https.request : http.request;
-		}
-
-		if (agents) {
-			const protocolName = options.protocol === 'https:' ? 'https' : 'http';
-			options.agent = (agents as AgentByProtocol)[protocolName] || options.agent;
-		}
-
-		/* istanbul ignore next: electron.net is broken */
-		// No point in typing process.versions correctly, as
-		// process.version.electron is used only once, right here.
-		if (options.useElectronNet && (process.versions as any).electron) {
-			const electron = dynamicRequire(module, 'electron'); // Trick webpack
-			requestFn = (electron as any).net.request || (electron as any).remote.net.request;
-		}
-
-		if (options.cookieJar) {
-			const cookieString = await getCookieString(currentUrl);
-
-			if (is.nonEmptyString(cookieString)) {
-				options.headers.cookie = cookieString;
-			}
-		}
+	const get = async (): Promise<void> => {
+		let httpOptions = await normalizeRequestArguments(options);
 
 		let timings: Timings;
 		const handleResponse = async (response: http.ServerResponse | ResponseObject): Promise<void> => {
-			options.timeout = timeout;
-
 			try {
 				/* istanbul ignore next: fixes https://github.com/electron/electron/blob/cbb460d47628a7a146adf4419ed48550a98b2923/lib/browser/api/net.js#L59-L65 */
 				if (options.useElectronNet) {
@@ -113,26 +65,28 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
 
 				const {statusCode} = response;
 				const typedResponse = response as Response;
+				// This is intentionally using `||` over `??` so it can also catch empty status message.
 				typedResponse.statusMessage = typedResponse.statusMessage || http.STATUS_CODES[statusCode];
-				typedResponse.url = currentUrl;
-				typedResponse.requestUrl = requestUrl;
+				typedResponse.url = options.url.toString();
+				typedResponse.requestUrl = requestURL;
 				typedResponse.retryCount = retryCount;
 				typedResponse.timings = timings;
 				typedResponse.redirectUrls = redirects;
 				typedResponse.request = {options};
-				typedResponse.isFromCache = typedResponse.fromCache || false;
+				typedResponse.isFromCache = typedResponse.fromCache ?? false;
 				delete typedResponse.fromCache;
 
 				if (!typedResponse.isFromCache) {
-					// @ts-ignore Node typings haven't been updated yet
+					// @ts-ignore Node.js typings haven't been updated yet
 					typedResponse.ip = response.socket.remoteAddress;
 				}
 
 				const rawCookies = typedResponse.headers['set-cookie'];
-				if (options.cookieJar && rawCookies) {
-					let promises: Array<Promise<unknown>> = rawCookies.map((rawCookie: string) => setCookie(rawCookie, typedResponse.url));
+				if (Reflect.has(options, 'cookieJar') && rawCookies) {
+					let promises: Array<Promise<unknown>> = rawCookies.map((rawCookie: string) => options.cookieJar.setCookie(rawCookie, typedResponse.url));
+
 					if (options.ignoreInvalidCookies) {
-						promises = promises.map(async p => p.catch(() => {}));
+						promises = promises.map(p => p.catch(() => {}));
 					}
 
 					await Promise.all(promises);
@@ -141,14 +95,16 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
 				if (options.followRedirect && Reflect.has(typedResponse.headers, 'location') && redirectCodes.has(statusCode)) {
 					typedResponse.resume(); // We're being redirected, we don't care about the response.
 
-					if (statusCode === 303 && options.method !== 'GET' && options.method !== 'HEAD') {
-						// Server responded with "see other", indicating that the resource exists at another location,
-						// and the client should request it from that location via GET or HEAD.
-						options.method = 'GET';
+					if (statusCode === 303) {
+						if (options.method !== 'GET' && options.method !== 'HEAD') {
+							// Server responded with "see other", indicating that the resource exists at another location,
+							// and the client should request it from that location via GET or HEAD.
+							options.method = 'GET';
+						}
 
+						delete options.body;
 						delete options.json;
 						delete options.form;
-						delete options.body;
 					}
 
 					if (redirects.length >= options.maxRedirects) {
@@ -157,31 +113,27 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
 
 					// Handles invalid URLs. See https://github.com/sindresorhus/got/issues/604
 					const redirectBuffer = Buffer.from(typedResponse.headers.location, 'binary').toString();
-					const redirectURL = new URL(redirectBuffer, currentUrl);
-					redirectString = redirectURL.toString();
+					const redirectURL = new URL(redirectBuffer, options.url);
 
-					redirects.push(redirectString);
+					// Redirecting to a different site, clear cookies.
+					if (redirectURL.hostname !== options.url.hostname) {
+						delete options.headers.cookie;
+					}
 
-					const redirectOptions = {
-						...options,
-						port: undefined,
-						auth: undefined,
-						...urlToOptions(redirectURL)
-					};
+					redirects.push(redirectURL.toString());
+					options.url = redirectURL;
 
 					for (const hook of options.hooks.beforeRedirect) {
 						// eslint-disable-next-line no-await-in-loop
-						await hook(redirectOptions, typedResponse);
+						await hook(options, typedResponse);
 					}
 
-					emitter.emit('redirect', response, redirectOptions);
+					emitter.emit('redirect', response, options);
 
-					await get(redirectOptions);
+					await get();
 					return;
 				}
 
-				delete options.body;
-
 				getResponse(typedResponse, options, emitter);
 			} catch (error) {
 				emitError(error);
@@ -195,14 +147,15 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
 			}
 
 			currentRequest = request;
-			options.timeout = timeout;
+
+			// `request.aborted` is a boolean since v11.0.0: https://github.com/nodejs/node/commit/4b00c4fafaa2ae8c41c1f78823c0feb810ae4723#diff-e3bc37430eb078ccbafe3aa3b570c91a
+			// We need to allow `TimedOutTimeoutError` here, because it `stream.pipeline(…)` aborts it automatically.
+			const isAborted = () => typeof request.aborted === 'number' || (request.aborted as unknown as boolean) === true;
 
 			const onError = (error: Error): void => {
 				const isTimedOutError = error instanceof TimedOutTimeoutError;
 
-				// `request.aborted` is a boolean since v11.0.0: https://github.com/nodejs/node/commit/4b00c4fafaa2ae8c41c1f78823c0feb810ae4723#diff-e3bc37430eb078ccbafe3aa3b570c91a
-				// We need to allow `TimedOutTimeoutError` here, because it `stream.pipeline(..)` aborts it automatically.
-				if (!isTimedOutError && (typeof request.aborted === 'number' || (request.aborted as unknown as boolean) === true)) {
+				if (!isTimedOutError && isAborted()) {
 					return;
 				}
 
@@ -225,7 +178,7 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
 				}
 
 				// No need to attach an error handler here,
-				// as `stream.pipeline(...)` doesn't remove this handler
+				// as `stream.pipeline(…)` doesn't remove this handler
 				// to allow stream reuse.
 
 				request.emit('upload-complete');
@@ -233,36 +186,34 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
 
 			request.on('error', onError);
 
-			timings = timer(request);
+			timings = timer(request); // TODO: Make `@szmarczak/http-timer` set `request.timings` and `response.timings`
 
+			const uploadBodySize = httpOptions.headers['content-length'] ? Number(httpOptions.headers['content-length']) : undefined;
 			uploadProgress(request, emitter, uploadBodySize);
 
-			if (options.timeout) {
-				timedOut(request, options.timeout, options);
-			}
+			timedOut(request, options.timeout, options.url);
 
 			emitter.emit('request', request);
 
+			if (isAborted()) {
+				return;
+			}
+
 			try {
-				if (is.nodeStream(options.body)) {
-					const {body} = options;
-					delete options.body;
-
-					// `stream.pipeline(...)` does it for us.
-					request.removeListener('error', onError);
-					stream.pipeline(
-						body,
-						request,
-						uploadComplete
-					);
-				} else if (options.body) {
-					request.end(options.body, uploadComplete);
-				} else if (input && (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH')) {
-					stream.pipeline(
-						input,
-						request,
-						uploadComplete
-					);
+				if (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH') {
+					if (is.nodeStream(httpOptions.body)) {
+						// `stream.pipeline(…)` handles `error` for us.
+						request.removeListener('error', onError);
+
+						stream.pipeline(
+							// @ts-ignore Upgrade `@sindresorhus/is`
+							httpOptions.body,
+							request,
+							uploadComplete
+						);
+					} else {
+						request.end(httpOptions.body, uploadComplete);
+					}
 				} else {
 					request.end(uploadComplete);
 				}
@@ -271,16 +222,14 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
 			}
 		};
 
-		const {timeout} = options;
-		delete options.timeout;
-
-		if (options.dnsCache) {
-			options.lookup = options.dnsCache.lookup;
-		}
-
 		if (options.cache) {
-			const cacheableRequest = new CacheableRequest(requestFn, options.cache);
-			const cacheRequest = cacheableRequest(options as unknown as https.RequestOptions, handleResponse);
+			// `cacheable-request` doesn't support Node 10 API, fallback.
+			httpOptions = {
+				...httpOptions,
+				...urlToOptions(options.url)
+			};
+
+			const cacheRequest = options.cacheableRequest(httpOptions, handleResponse);
 
 			cacheRequest.once('error', error => {
 				if (error instanceof CacheableRequest.RequestError) {
@@ -292,17 +241,18 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
 
 			cacheRequest.once('request', handleRequest);
 		} else {
-			// Catches errors thrown by calling requestFn(...)
+			// Catches errors thrown by calling `requestFn(…)`
 			try {
-				// @ts-ignore TS complains that URLSearchParams is not the same as URLSearchParams
-				handleRequest(requestFn(options as unknown as URL, handleResponse));
+				// @ts-ignore 1. TS complains that URLSearchParams is not the same as URLSearchParams.
+				//            2. It doesn't notice that `options.timeout` is deleted above.
+				handleRequest(httpOptions.request(options.url, httpOptions, handleResponse));
 			} catch (error) {
 				emitError(new RequestError(error, options));
 			}
 		}
 	};
 
-	emitter.retry = (error): boolean => {
+	emitter.retry = error => {
 		let backoff: number;
 
 		retryCount++;
@@ -332,7 +282,7 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
 						await hook(options, error, retryCount);
 					}
 
-					await get(options);
+					await get();
 				} catch (error_) {
 					emitError(error_);
 				}
@@ -360,59 +310,7 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
 				await hook(options);
 			}
 
-			// Serialize body
-			const {body, headers} = options;
-			const isForm = !is.nullOrUndefined(options.form);
-			const isJSON = !is.nullOrUndefined(options.json);
-			const isBody = !is.nullOrUndefined(body);
-			if ((isBody || isForm || isJSON) && withoutBody.has(options.method)) {
-				throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
-			}
-
-			if (isBody) {
-				if (isForm || isJSON) {
-					throw new TypeError('The `body` option cannot be used with the `json` option or `form` option');
-				}
-
-				if (is.object(body) && isFormData(body)) {
-					// Special case for https://github.com/form-data/form-data
-					headers['content-type'] = headers['content-type'] || `multipart/form-data; boundary=${body.getBoundary()}`;
-				} else if (!is.nodeStream(body) && !is.string(body) && !is.buffer(body)) {
-					throw new TypeError('The `body` option must be a stream.Readable, string or Buffer');
-				}
-			} else if (isForm) {
-				if (!is.object(options.form)) {
-					throw new TypeError('The `form` option must be an Object');
-				}
-
-				headers['content-type'] = headers['content-type'] || 'application/x-www-form-urlencoded';
-				options.body = (new URLSearchParams(options.form as Record<string, string>)).toString();
-			} else if (isJSON) {
-				headers['content-type'] = headers['content-type'] || 'application/json';
-				options.body = JSON.stringify(options.json);
-			}
-
-			// Convert buffer to stream to receive upload progress events (#322)
-			if (is.buffer(body)) {
-				options.body = toReadableStream(body);
-				uploadBodySize = body.length;
-			} else {
-				uploadBodySize = await getBodySize(options);
-			}
-
-			if (is.undefined(headers['content-length']) && is.undefined(headers['transfer-encoding'])) {
-				if ((uploadBodySize > 0 || options.method === 'PUT') && !is.undefined(uploadBodySize)) {
-					headers['content-length'] = String(uploadBodySize);
-				}
-			}
-
-			if (!options.stream && options.responseType === 'json' && is.undefined(headers.accept)) {
-				options.headers.accept = 'application/json';
-			}
-
-			requestUrl = options.href || (new URL(options.path, format(options as UrlObject))).toString();
-
-			await get(options);
+			await get();
 		} catch (error) {
 			emitError(error);
 		}
@@ -420,3 +318,18 @@ export default (options: NormalizedOptions, input?: TransformStream) => {
 
 	return emitter;
 };
+
+export const proxyEvents = (proxy, emitter) => {
+	const events = [
+		'request',
+		'redirect',
+		'uploadProgress',
+		'downloadProgress'
+	];
+
+	for (const event of events) {
+		emitter.on(event, (...args: unknown[]) => {
+			proxy.emit(event, ...args);
+		});
+	}
+};
diff --git a/source/utils/dynamic-require.ts b/source/utils/dynamic-require.ts
index 11887df..515f719 100644
--- a/source/utils/dynamic-require.ts
+++ b/source/utils/dynamic-require.ts
@@ -1,5 +1,3 @@
 /* istanbul ignore file: used for webpack */
 
-export default (moduleObject: NodeModule, moduleId: string): unknown => {
-	return moduleObject.require(moduleId);
-};
+export default (moduleObject: NodeModule, moduleId: string): unknown => moduleObject.require(moduleId);
diff --git a/source/utils/get-body-size.ts b/source/utils/get-body-size.ts
index 63a1e1b..713bd19 100644
--- a/source/utils/get-body-size.ts
+++ b/source/utils/get-body-size.ts
@@ -7,13 +7,13 @@ import {Options} from './types';
 const statAsync = promisify(stat);
 
 export default async (options: Options): Promise<number | undefined> => {
-	const {body, headers, stream} = options;
+	const {body, headers, isStream} = options;
 
 	if (headers && 'content-length' in headers) {
 		return Number(headers['content-length']);
 	}
 
-	if (!body && !stream) {
+	if (!body && !isStream) {
 		return 0;
 	}
 
diff --git a/source/utils/merge.ts b/source/utils/merge.ts
new file mode 100644
index 0000000..b34e4c3
--- /dev/null
+++ b/source/utils/merge.ts
@@ -0,0 +1,30 @@
+import is from '@sindresorhus/is';
+
+export default function merge<Target extends Record<string, any>, Source extends Record<string, any>>(target: Target, ...sources: Source[]): Target & Source {
+	for (const source of sources) {
+		for (const [key, sourceValue] of Object.entries(source)) {
+			const targetValue = target[key];
+
+			if (is.urlInstance(targetValue) && is.string(sourceValue)) {
+				// @ts-ignore
+				target[key] = new URL(sourceValue, targetValue);
+			} else if (is.plainObject(sourceValue)) {
+				if (is.plainObject(targetValue)) {
+					// @ts-ignore
+					target[key] = merge({}, targetValue, sourceValue);
+				} else {
+					// @ts-ignore
+					target[key] = merge({}, sourceValue);
+				}
+			} else if (is.array(sourceValue)) {
+				// @ts-ignore
+				target[key] = sourceValue.slice();
+			} else {
+				// @ts-ignore
+				target[key] = sourceValue;
+			}
+		}
+	}
+
+	return target as Target & Source;
+}
diff --git a/source/utils/options-to-url.ts b/source/utils/options-to-url.ts
new file mode 100644
index 0000000..1a0bd10
--- /dev/null
+++ b/source/utils/options-to-url.ts
@@ -0,0 +1,84 @@
+function validateSearchParams(searchParams: Record<string, unknown>): asserts searchParams is Record<string, string | number | boolean | null> {
+	for (const value of Object.values(searchParams)) {
+		if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean' && value !== null) {
+			throw new TypeError(`The \`searchParams\` value '${value}' must be a string, number, boolean or null`);
+		}
+	}
+}
+
+export interface URLOptions {
+	href?: string;
+	origin?: string;
+	protocol?: string;
+	username?: string;
+	password?: string;
+	host?: string;
+	hostname?: string;
+	port?: string | number;
+	pathname?: string;
+	search?: string;
+	searchParams?: Record<string, string | number | boolean | null> | URLSearchParams | string;
+	hash?: string;
+}
+
+const keys = [
+	'protocol',
+	'username',
+	'password',
+	'host',
+	'hostname',
+	'port',
+	'pathname',
+	'search',
+	'hash'
+];
+
+export default (options: URLOptions): URL => {
+	let origin: string;
+
+	if (Reflect.has(options, 'path')) {
+		throw new TypeError('Parameter `path` is deprecated. Use `pathname` instead.');
+	}
+
+	if (Reflect.has(options, 'auth')) {
+		throw new TypeError('Parameter `auth` is deprecated. Use `username`/`password` instead.');
+	}
+
+	if (options.search && options.searchParams) {
+		throw new TypeError('Parameters `search` and `searchParams` are mutually exclusive.');
+	}
+
+	if (options.href) {
+		return new URL(options.href);
+	}
+
+	if (options.origin) {
+		origin = options.origin;
+	} else {
+		if (!options.protocol) {
+			throw new TypeError('No URL protocol specified');
+		}
+
+		origin = `${options.protocol}//${options.hostname || options.host}`;
+	}
+
+	const url = new URL(origin);
+
+	for (const key of keys) {
+		if (Reflect.has(options, key)) {
+			url[key] = options[key];
+		}
+	}
+
+	if (Reflect.has(options, 'searchParams')) {
+		if (typeof options.searchParams !== 'string' && !(options.searchParams instanceof URLSearchParams)) {
+			validateSearchParams(options.searchParams);
+		}
+
+		(new URLSearchParams(options.searchParams as Record<string, string>)).forEach((value, key) => {
+			url.searchParams.append(key, value);
+		});
+	}
+
+	return url;
+};
diff --git a/source/utils/timed-out.ts b/source/utils/timed-out.ts
index 5d05d78..bd22cc5 100644
--- a/source/utils/timed-out.ts
+++ b/source/utils/timed-out.ts
@@ -3,7 +3,7 @@ import {ClientRequest, IncomingMessage} from 'http';
 import {Delays} from './types';
 import unhandler from './unhandle';
 
-const reentry = Symbol('reentry');
+const reentry: unique symbol = Symbol('reentry');
 const noop = (): void => {};
 
 interface TimedOutOptions {
@@ -40,13 +40,10 @@ export default (request: ClientRequest, delays: Delays, options: TimedOutOptions
 		const timeout: NodeJS.Timeout = setTimeout(() => {
 			// @ts-ignore https://github.com/microsoft/TypeScript/issues/26113
 			immediate = setImmediate(callback, delay, ...args);
-			immediate.unref();
+			immediate.unref?.();
 		}, delay);
 
-		/* istanbul ignore next: in order to support electron renderer */
-		if (timeout.unref) {
-			timeout.unref();
-		}
+		timeout.unref?.();
 
 		const cancel = (): void => {
 			clearTimeout(timeout);
@@ -103,12 +100,14 @@ export default (request: ClientRequest, delays: Delays, options: TimedOutOptions
 	}
 
 	once(request, 'socket', (socket: net.Socket): void => {
-		// TODO: There seems to not be a 'socketPath' on the request, but there IS a socket.remoteAddress
-		const {socketPath} = request as any;
+		// @ts-ignore Node typings doesn't have this property
+		const {socketPath} = request;
 
 		/* istanbul ignore next: hard to test */
 		if (socket.connecting) {
-			if (typeof delays.lookup !== 'undefined' && !socketPath && !net.isIP(hostname || host || '') && typeof (socket.address() as net.AddressInfo).address === 'undefined') {
+			const hasPath = Boolean(socketPath ?? net.isIP(hostname ?? host ?? '') !== 0);
+
+			if (typeof delays.lookup !== 'undefined' && !hasPath && typeof (socket.address() as net.AddressInfo).address === 'undefined') {
 				const cancelTimeout = addTimeout(delays.lookup, timeoutHandler, 'lookup');
 				once(socket, 'lookup', cancelTimeout);
 			}
@@ -116,7 +115,7 @@ export default (request: ClientRequest, delays: Delays, options: TimedOutOptions
 			if (typeof delays.connect !== 'undefined') {
 				const timeConnect = (): (() => void) => addTimeout(delays.connect, timeoutHandler, 'connect');
 
-				if (socketPath || net.isIP(hostname || host || '')) {
+				if (hasPath) {
 					once(socket, 'connect', timeConnect());
 				} else {
 					once(socket, 'lookup', (error: Error): void => {
diff --git a/source/utils/types.ts b/source/utils/types.ts
index c7ca055..571ab45 100644
--- a/source/utils/types.ts
+++ b/source/utils/types.ts
@@ -2,16 +2,18 @@ import http = require('http');
 import https = require('https');
 import ResponseLike = require('responselike');
 import {Readable as ReadableStream} from 'stream';
+import {Except, Merge} from 'type-fest';
 import PCancelable = require('p-cancelable');
-import {CookieJar} from 'tough-cookie';
-import {StorageAdapter} from 'cacheable-request';
-import {Except} from 'type-fest';
+import CacheableRequest = require('cacheable-request');
 import CacheableLookup from 'cacheable-lookup';
 import Keyv = require('keyv');
 import {Timings} from '@szmarczak/http-timer/dist';
 import {Hooks} from '../known-hook-events';
 import {GotError, ParseError, HTTPError, MaxRedirectsError} from '../errors';
 import {ProxyStream} from '../as-stream';
+import {URLOptions} from './options-to-url';
+
+export type GenericError = Error | GotError | ParseError | HTTPError | MaxRedirectsError;
 
 export type Method =
 	| 'GET'
@@ -41,12 +43,10 @@ export type ErrorCode =
 	| 'ENETUNREACH'
 	| 'EAI_AGAIN';
 
-export type ResponseType = 'json' | 'buffer' | 'text';
-
-export type URLArgument = string | https.RequestOptions | URL;
+export type ResponseType = 'json' | 'buffer' | 'text' | 'default';
 
-export interface Response extends http.IncomingMessage {
-	body: Buffer | string | any;
+export interface Response<BodyType = unknown> extends http.IncomingMessage {
+	body: BodyType;
 	statusCode: number;
 
 	/**
@@ -79,7 +79,7 @@ export interface ResponseObject extends ResponseLike {
 export interface RetryObject {
 	attemptCount: number;
 	retryOptions: RetryOptions;
-	error: Error | GotError | ParseError | HTTPError | MaxRedirectsError;
+	error: GenericError;
 	computedValue: number;
 }
 
@@ -115,19 +115,22 @@ export interface Delays {
 
 export type Headers = Record<string, string | string[]>;
 
-// The library overrides the type definition of `agent`, `host`, 'headers and `timeout`.
-export interface Options extends Except<https.RequestOptions, 'agent' | 'timeout' | 'host' | 'headers'> {
-	host?: string;
+interface CookieJar {
+	getCookieString(url: string, callback: (error: Error, cookieHeader: string) => void): void;
+	getCookieString(url: string): Promise<string>;
+	setCookie(rawCookie: string, url: string, callback: (error: Error, result: unknown) => void): void;
+	setCookie(rawCookie: string, url: string): Promise<unknown>;
+}
+
+// TODO: Missing lots of `http` options
+export interface Options extends URLOptions {
+	url?: URL | string;
 	body?: string | Buffer | ReadableStream;
 	hostname?: string;
-	path?: string;
 	socketPath?: string;
-	protocol?: string;
-	href?: string;
-	options?: Options;
 	hooks?: Partial<Hooks>;
 	decompress?: boolean;
-	stream?: boolean;
+	isStream?: boolean;
 	encoding?: BufferEncoding | null;
 	method?: Method;
 	retry?: number | RetryOptions;
@@ -136,7 +139,7 @@ export interface Options extends Except<https.RequestOptions, 'agent' | 'timeout
 	ignoreInvalidCookies?: boolean;
 	request?: RequestFunction;
 	agent?: http.Agent | https.Agent | boolean | AgentByProtocol;
-	cache?: string | StorageAdapter | false;
+	cache?: string | CacheableRequest.StorageAdapter | false;
 	headers?: Headers;
 	responseType?: ResponseType;
 	resolveBodyOnly?: boolean;
@@ -144,9 +147,6 @@ export interface Options extends Except<https.RequestOptions, 'agent' | 'timeout
 	prefixUrl?: URL | string;
 	timeout?: number | Delays;
 	dnsCache?: CacheableLookup | Map<string, string> | Keyv | false;
-	url?: URL | string;
-	searchParams?: Record<string, string | number | boolean | null> | URLSearchParams | string;
-	query?: Options['searchParams']; // Deprecated
 	useElectronNet?: boolean;
 	form?: Record<string, any>;
 	json?: Record<string, any>;
@@ -155,29 +155,20 @@ export interface Options extends Except<https.RequestOptions, 'agent' | 'timeout
 	lookup?: CacheableLookup['lookup'];
 }
 
-export interface NormalizedOptions extends Except<Options, 'method'> {
+export interface NormalizedOptions extends Except<Options, keyof URLOptions> {
 	// Normalized Got options
 	headers: Headers;
 	hooks: Hooks;
 	timeout: Delays;
 	dnsCache?: CacheableLookup | false;
 	retry: Required<RetryOptions>;
-	readonly prefixUrl?: string;
+	prefixUrl: string;
 	method: Method;
+	url: URL;
+	cacheableRequest?: (options: string | URL | http.RequestOptions, callback?: (response: http.ServerResponse | ResponseLike) => void) => CacheableRequest.Emitter;
 
-	// Normalized URL options
-	protocol: string;
-	hostname: string;
-	host: string;
-	hash: string;
-	search: string | null;
-	pathname: string;
-	href: string;
-	path: string;
-	port: number;
-	username: string;
-	password: string;
-	auth?: string;
+	// UNIX socket support
+	path?: string;
 }
 
 export interface ExtendedOptions extends Options {
@@ -186,22 +177,29 @@ export interface ExtendedOptions extends Options {
 }
 
 export interface Defaults {
-	options?: Options;
-	handlers?: HandlerFunction[];
-	mutableDefaults?: boolean;
-}
-
-export interface NormalizedDefaults {
-	options: NormalizedOptions;
+	options: Except<NormalizedOptions, 'url'>;
 	handlers: HandlerFunction[];
+	_rawHandlers?: HandlerFunction[];
 	mutableDefaults: boolean;
 }
 
-export type URLOrOptions = URLArgument | (Options & {url: URLArgument});
+export type URLOrOptions = Options | string;
+
+export interface Progress {
+	percent: number;
+	transferred: number;
+	total?: number;
+}
+
+export interface GotEvents<T> {
+	on(name: 'request', listener: (request: http.ClientRequest) => void): T;
+	on(name: 'response', listener: (response: Response) => void): T;
+	on(name: 'redirect', listener: (response: Response, nextOptions: NormalizedOptions) => void): T;
+	on(name: 'uploadProgress' | 'downloadProgress', listener: (progress: Progress) => void): T;
+}
 
-export interface CancelableRequest<T extends http.IncomingMessage | Buffer | string | object> extends PCancelable<T> {
-	on(name: string, listener: () => void): CancelableRequest<T>;
+export interface CancelableRequest<T extends Response | Response['body']> extends Merge<PCancelable<T>, GotEvents<CancelableRequest<T>>> {
 	json<TReturnType extends object>(): CancelableRequest<TReturnType>;
-	buffer<TReturnType extends Buffer>(): CancelableRequest<TReturnType>;
-	text<TReturnType extends string>(): CancelableRequest<TReturnType>;
+	buffer(): CancelableRequest<Buffer>;
+	text(): CancelableRequest<string>;
 }
diff --git a/source/utils/url-to-options.ts b/source/utils/url-to-options.ts
index 3cf6e2a..2e274b0 100644
--- a/source/utils/url-to-options.ts
+++ b/source/utils/url-to-options.ts
@@ -3,7 +3,7 @@ import is from '@sindresorhus/is';
 
 // TODO: Deprecate legacy Url at some point
 
-export interface URLOptions {
+export interface LegacyURLOptions {
 	protocol: string;
 	hostname: string;
 	host: string;
@@ -16,11 +16,11 @@ export interface URLOptions {
 	auth?: string;
 }
 
-export default (url: URL | UrlWithStringQuery): URLOptions => {
+export default (url: URL | UrlWithStringQuery): LegacyURLOptions => {
 	// Cast to URL
 	url = url as URL;
 
-	const options: URLOptions = {
+	const options: LegacyURLOptions = {
 		protocol: url.protocol,
 		hostname: url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname,
 		host: url.host,
diff --git a/source/utils/validate-search-params.ts b/source/utils/validate-search-params.ts
deleted file mode 100644
index f02f9c3..0000000
--- a/source/utils/validate-search-params.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import is from '@sindresorhus/is';
-
-export default (searchParams: Record<string, unknown>): void => {
-	for (const value of Object.values(searchParams)) {
-		if (!is.string(value) && !is.number(value) && !is.boolean(value) && !is.null_(value)) {
-			throw new TypeError(`The \`searchParams\` value '${value}' must be a string, number, boolean or null`);
-		}
-	}
-};


Test patch:
diff --git a/test/arguments.ts b/test/arguments.ts
index 14041f5..df3ea31 100644
--- a/test/arguments.ts
+++ b/test/arguments.ts
@@ -12,10 +12,10 @@ const echoUrl = (request, response) => {
 test('`url` is required', async t => {
 	await t.throwsAsync(
 		// @ts-ignore Manual tests
-		got(),
+		got(''),
 		{
 			instanceOf: TypeError,
-			message: 'Parameter `url` must be a string or an Object, not undefined'
+			message: 'No URL protocol specified'
 		}
 	);
 });
@@ -29,6 +29,11 @@ test('`url` should be utf-8 encoded', async t => {
 	);
 });
 
+test('throws if no arguments provided', async t => {
+	// @ts-ignore This is on purpose.
+	await t.throwsAsync(got(), TypeError, 'Missing `url` argument');
+});
+
 test('throws an error if the protocol is not specified', async t => {
 	await t.throwsAsync(got('example.com'), {
 		instanceOf: TypeError,
@@ -39,6 +44,11 @@ test('throws an error if the protocol is not specified', async t => {
 		instanceOf: TypeError,
 		message: 'No URL protocol specified'
 	});
+
+	await t.throwsAsync(got({}), {
+		instanceOf: TypeError,
+		message: 'No URL protocol specified'
+	});
 });
 
 test('string url with searchParams is preserved', withServer, async (t, server, got) => {
@@ -58,8 +68,7 @@ test('options are optional', withServer, async (t, server, got) => {
 test('methods are normalized', withServer, async (t, server, got) => {
 	server.post('/test', echoUrl);
 
-	const instance = got.create({
-		options: got.defaults.options,
+	const instance = got.extend({
 		handlers: [
 			(options, next) => {
 				if (options.method === options.method.toUpperCase()) {
@@ -76,16 +85,13 @@ test('methods are normalized', withServer, async (t, server, got) => {
 	await instance('test', {method: 'post'});
 });
 
-test('accepts url.parse object as first argument', withServer, async (t, server, got) => {
+test('throws an error when legacy Url is passed', withServer, async (t, server, got) => {
 	server.get('/test', echoUrl);
 
-	t.is((await got(parse(`${server.url}/test`))).body, '/test');
-});
-
-test('requestUrl with url.parse object as first argument', withServer, async (t, server, got) => {
-	server.get('/test', echoUrl);
-
-	t.is((await got(parse(`${server.url}/test`))).requestUrl, `${server.url}/test`);
+	await t.throwsAsync(
+		got(parse(`${server.url}/test`)),
+		'The legacy `url.Url` is deprecated. Use `URL` instead.'
+	);
 });
 
 test('overrides `searchParams` from options', withServer, async (t, server, got) => {
@@ -137,9 +143,9 @@ test('ignores empty searchParams object', withServer, async (t, server, got) =>
 	t.is((await got('test', {searchParams: {}})).requestUrl, `${server.url}/test`);
 });
 
-test('throws on invalid type of body', async t => {
+test('throws when passing body with a non payload method', async t => {
 	// @ts-ignore Manual tests
-	await t.throwsAsync(got('https://example.com', {body: false}), {
+	await t.throwsAsync(got('https://example.com', {body: 'asdf'}), {
 		instanceOf: TypeError,
 		message: 'The `GET` method cannot be used with a body'
 	});
@@ -152,12 +158,12 @@ test('WHATWG URL support', withServer, async (t, server, got) => {
 	await t.notThrowsAsync(got(wURL));
 });
 
-test('returns streams when using stream option', withServer, async (t, server, got) => {
+test('returns streams when using `isStream` option', withServer, async (t, server, got) => {
 	server.get('/stream', (_request, response) => {
 		response.end('ok');
 	});
 
-	const data = await pEvent(got('stream', {stream: true}), 'data');
+	const data = await pEvent(got('stream', {isStream: true}), 'data');
 	t.is(data.toString(), 'ok');
 });
 
@@ -236,19 +242,36 @@ test('backslash in the end of `prefixUrl` option is optional', withServer, async
 	t.is(body, '/test/foobar');
 });
 
-test('throws when trying to modify `prefixUrl` after options got normalized', async t => {
-	const instanceA = got.create({
-		options: {prefixUrl: 'https://example.com'},
+test('`prefixUrl` can be changed if the URL contains the old one', withServer, async (t, server) => {
+	server.get('/', echoUrl);
+
+	const instanceA = got.extend({
+		prefixUrl: `${server.url}/meh`,
+		handlers: [
+			(options, next) => {
+				options.prefixUrl = server.url;
+				return next(options);
+			}
+		]
+	});
+
+	const {body} = await instanceA('');
+	t.is(body, '/');
+});
+
+test('throws if cannot change `prefixUrl`', async t => {
+	const instanceA = got.extend({
+		prefixUrl: 'https://example.com',
 		handlers: [
 			(options, next) => {
-				// @ts-ignore Even though we know it's read only, we need to test it.
-				options.prefixUrl = 'https://google.com';
+				options.url = new URL('https://google.pl');
+				options.prefixUrl = 'https://example.com';
 				return next(options);
 			}
 		]
 	});
 
-	await t.throwsAsync(instanceA(''), 'Failed to set prefixUrl. Options are normalized already.');
+	await t.throwsAsync(instanceA(''), 'Cannot change `prefixUrl` from https://example.com/ to https://example.com: https://google.pl/');
 });
 
 test('throws if the `searchParams` value is invalid', async t => {
@@ -313,67 +336,3 @@ test('`context` option is accessible when extending instances', t => {
 	t.is(instance.defaults.options.context, context);
 	t.false({}.propertyIsEnumerable.call(instance.defaults.options, 'context'));
 });
-
-test('`options.body` is cleaned up when retrying - `options.json`', withServer, async (t, server, got) => {
-	let first = true;
-	server.post('/', (_request, response) => {
-		if (first) {
-			first = false;
-
-			response.statusCode = 401;
-		}
-
-		response.end();
-	});
-
-	await t.notThrowsAsync(got.post('', {
-		hooks: {
-			afterResponse: [
-				async (response, retryWithMergedOptions) => {
-					if (response.statusCode === 401) {
-						return retryWithMergedOptions();
-					}
-
-					t.is(response.request.options.body, undefined);
-
-					return response;
-				}
-			]
-		},
-		json: {
-			some: 'data'
-		}
-	}));
-});
-
-test('`options.body` is cleaned up when retrying - `options.form`', withServer, async (t, server, got) => {
-	let first = true;
-	server.post('/', (_request, response) => {
-		if (first) {
-			first = false;
-
-			response.statusCode = 401;
-		}
-
-		response.end();
-	});
-
-	await t.notThrowsAsync(got.post('', {
-		hooks: {
-			afterResponse: [
-				async (response, retryWithMergedOptions) => {
-					if (response.statusCode === 401) {
-						return retryWithMergedOptions();
-					}
-
-					t.is(response.request.options.body, undefined);
-
-					return response;
-				}
-			]
-		},
-		form: {
-			some: 'data'
-		}
-	}));
-});
diff --git a/test/cache.ts b/test/cache.ts
index b7c5d30..648bf88 100644
--- a/test/cache.ts
+++ b/test/cache.ts
@@ -92,14 +92,14 @@ test('cached response has got options', withServer, async (t, server, got) => {
 
 	const cache = new Map();
 	const options = {
-		auth: 'foo:bar',
+		username: 'foo',
 		cache
 	};
 
 	await got(options);
 	const secondResponse = await got(options);
 
-	t.is(secondResponse.request.options.auth, options.auth);
+	t.is(secondResponse.request.options.username, options.username);
 });
 
 test('cache error throws `got.CacheError`', withServer, async (t, server, got) => {
diff --git a/test/cancel.ts b/test/cancel.ts
index 2ac55e3..306b5ae 100644
--- a/test/cancel.ts
+++ b/test/cancel.ts
@@ -59,7 +59,7 @@ test.serial('does not retry after cancelation', withServerAndLolex, async (t, se
 
 	const gotPromise = got('redirect', {
 		retry: {
-			retries: () => {
+			calculateDelay: () => {
 				t.fail('Makes a new try after cancelation');
 			}
 		}
diff --git a/test/cookies.ts b/test/cookies.ts
index 71a305f..bb99e62 100644
--- a/test/cookies.ts
+++ b/test/cookies.ts
@@ -50,7 +50,7 @@ test('cookies doesn\'t break on redirects', withServer, async (t, server, got) =
 	});
 
 	server.get('/', (request, response) => {
-		response.end(request.headers.cookie || '');
+		response.end(request.headers.cookie ?? '');
 	});
 
 	const cookieJar = new toughCookie.CookieJar();
@@ -108,7 +108,7 @@ test('overrides options.headers.cookie', withServer, async (t, server, got) => {
 	});
 
 	server.get('/', (request, response) => {
-		response.end(request.headers.cookie || '');
+		response.end(request.headers.cookie ?? '');
 	});
 
 	const cookieJar = new toughCookie.CookieJar();
@@ -130,8 +130,10 @@ test('no unhandled errors', async t => {
 
 	const options = {
 		cookieJar: {
-			setCookie: () => {},
-			getCookieString: (_, cb) => cb(new Error(message))
+			setCookie: async (_rawCookie, _url) => {},
+			getCookieString: async _url => {
+				throw new Error(message);
+			}
 		}
 	};
 
@@ -142,3 +144,48 @@ test('no unhandled errors', async t => {
 
 	server.close();
 });
+
+test('accepts custom `cookieJar` object', withServer, async (t, server, got) => {
+	server.get('/', (request, response) => {
+		response.setHeader('set-cookie', ['hello=world']);
+		response.end(request.headers.cookie);
+	});
+
+	const cookies = {};
+	const cookieJar = {
+		async getCookieString(url) {
+			t.is(typeof url, 'string');
+
+			return cookies[url];
+		},
+
+		async setCookie(rawCookie, url) {
+			cookies[url] = rawCookie;
+		}
+	};
+
+	const first = await got('', {cookieJar});
+	const second = await got('', {cookieJar});
+
+	t.is(first.body, '');
+	t.is(second.body, 'hello=world');
+});
+
+test('throws on invalid `options.cookieJar.setCookie`', async t => {
+	await t.throwsAsync(got('https://example.com', {
+		cookieJar: {
+			// @ts-ignore
+			setCookie: () => {}
+		}
+	}), '`options.cookieJar.setCookie` needs to be an async function with 2 arguments');
+});
+
+test('throws on invalid `options.cookieJar.getCookieString`', async t => {
+	await t.throwsAsync(got('https://example.com', {
+		cookieJar: {
+			setCookie: async (_rawCookie, _url) => {},
+			// @ts-ignore
+			getCookieString: () => {}
+		}
+	}), '`options.cookieJar.getCookieString` needs to be an async function with 1 argument');
+});
diff --git a/test/create.ts b/test/create.ts
index 494195b..06b75b3 100644
--- a/test/create.ts
+++ b/test/create.ts
@@ -103,28 +103,11 @@ test('extend merges URL instances', t => {
 	t.is(b.defaults.options.custom.toString(), 'https://example.com/foo');
 });
 
-test('create', withServer, async (t, server) => {
-	server.all('/', echoHeaders);
-
-	const instance = got.create({
-		options: {},
-		handlers: [
-			(options, next) => {
-				options.headers.unicorn = 'rainbow';
-				return next(options);
-			}
-		]
-	});
-	const headers = await instance(server.url).json<TestReturn>();
-	t.is(headers.unicorn, 'rainbow');
-	t.is(headers['user-agent'], undefined);
-});
-
 test('hooks are merged on got.extend()', t => {
 	const hooksA = [() => {}];
 	const hooksB = [() => {}];
 
-	const instanceA = got.create({options: {hooks: {beforeRequest: hooksA}}});
+	const instanceA = got.extend({hooks: {beforeRequest: hooksA}});
 
 	const extended = instanceA.extend({hooks: {beforeRequest: hooksB}});
 	t.deepEqual(extended.defaults.options.hooks.beforeRequest, hooksA.concat(hooksB));
@@ -140,33 +123,17 @@ test('custom endpoint with custom headers (extend)', withServer, async (t, serve
 });
 
 test('no tampering with defaults', t => {
-	const instance = got.create({
-		handlers: got.defaults.handlers,
-		options: got.mergeOptions(got.defaults.options, {
-			prefixUrl: 'example/'
-		})
-	});
-
-	const instance2 = instance.create({
-		handlers: instance.defaults.handlers,
-		options: instance.defaults.options
-	});
-
-	// Tamper Time
 	t.throws(() => {
-		instance.defaults.options.prefixUrl = 'http://google.com';
+		got.defaults.options.prefixUrl = 'http://google.com';
 	});
 
-	t.is(instance.defaults.options.prefixUrl, 'example/');
-	t.is(instance2.defaults.options.prefixUrl, 'example/');
+	t.is(got.defaults.options.prefixUrl, '');
 });
 
 test('defaults can be mutable', t => {
-	const instance = got.create({
+	const instance = got.extend({
 		mutableDefaults: true,
-		options: {
-			followRedirect: false
-		}
+		followRedirect: false
 	});
 
 	t.notThrows(() => {
@@ -201,7 +168,7 @@ test('only plain objects are freezed', withServer, async (t, server, got) => {
 
 test('defaults are cloned on instance creation', t => {
 	const options = {foo: 'bar', hooks: {beforeRequest: [() => {}]}};
-	const instance = got.create({options});
+	const instance = got.extend(options);
 
 	t.notThrows(() => {
 		options.foo = 'foo';
diff --git a/test/error.ts b/test/error.ts
index d429a48..117c263 100644
--- a/test/error.ts
+++ b/test/error.ts
@@ -24,10 +24,7 @@ test('properties', withServer, async (t, server, got) => {
 	t.false({}.propertyIsEnumerable.call(error, 'response'));
 	t.false({}.hasOwnProperty.call(error, 'code'));
 	t.is(error.message, 'Response code 404 (Not Found)');
-	t.is(error.options.host, `${url.hostname}:${url.port}`);
-	t.is(error.options.method, 'GET');
-	t.is(error.options.protocol, 'http:');
-	t.is(error.options.url, error.options.requestUrl);
+	t.deepEqual(error.options.url, url);
 	t.is(error.response.headers.connection, 'close');
 	t.is(error.response.body, 'not');
 });
@@ -36,14 +33,14 @@ test('catches dns errors', async t => {
 	const error = await t.throwsAsync<GotError>(got('http://doesntexist', {retry: 0}));
 	t.truthy(error);
 	t.regex(error.message, /getaddrinfo ENOTFOUND/);
-	t.is(error.options.host, 'doesntexist');
+	t.is(error.options.url.host, 'doesntexist');
 	t.is(error.options.method, 'GET');
 });
 
 test('`options.body` form error message', async t => {
 	// @ts-ignore Manual tests
 	await t.throwsAsync(got.post('https://example.com', {body: Buffer.from('test'), form: ''}), {
-		message: 'The `body` option cannot be used with the `json` option or `form` option'
+		message: 'The `body`, `json` and `form` options are mutually exclusive'
 	});
 });
 
@@ -108,12 +105,12 @@ test('contains Got options', withServer, async (t, server, got) => {
 	});
 
 	const options = {
-		auth: 'foo:bar'
+		agent: false
 	};
 
 	const error = await t.throwsAsync(got(options));
 	// @ts-ignore
-	t.is(error.options.auth, options.auth);
+	t.is(error.options.agent, options.agent);
 });
 
 test('empty status message is overriden by the default one', withServer, async (t, server, got) => {
@@ -197,7 +194,7 @@ test('catches error in mimicResponse', withServer, async (t, server) => {
 test('errors are thrown directly when options.stream is true', t => {
 	t.throws(() => {
 		// @ts-ignore Manual tests
-		got('https://example.com', {stream: true, hooks: false});
+		got('https://example.com', {isStream: true, hooks: false});
 	}, {
 		message: 'Parameter `hooks` must be an Object, not boolean'
 	});
diff --git a/test/gzip.ts b/test/gzip.ts
index 4af3f06..7f25614 100644
--- a/test/gzip.ts
+++ b/test/gzip.ts
@@ -36,11 +36,10 @@ test('handles gzip error', withServer, async (t, server, got) => {
 		response.end('Not gzipped content');
 	});
 
-	const error = await t.throwsAsync(got(''), 'incorrect header check');
-
-	// @ts-ignore
-	t.is(error.options.path, '/');
-	t.is(error.name, 'ReadError');
+	await t.throwsAsync(got(''), {
+		name: 'ReadError',
+		message: 'incorrect header check'
+	});
 });
 
 test('handles gzip error - stream', withServer, async (t, server, got) => {
@@ -49,11 +48,10 @@ test('handles gzip error - stream', withServer, async (t, server, got) => {
 		response.end('Not gzipped content');
 	});
 
-	const error = await t.throwsAsync(getStream(got.stream('')), 'incorrect header check');
-
-	// @ts-ignore
-	t.is(error.options.path, '/');
-	t.is(error.name, 'ReadError');
+	await t.throwsAsync(getStream(got.stream('')), {
+		name: 'ReadError',
+		message: 'incorrect header check'
+	});
 });
 
 test('decompress option opts out of decompressing', withServer, async (t, server, got) => {
diff --git a/test/headers.ts b/test/headers.ts
index 0834b99..5da21cb 100644
--- a/test/headers.ts
+++ b/test/headers.ts
@@ -52,7 +52,8 @@ test('does not remove user headers from `url` object argument', withServer, asyn
 		}
 	})).body;
 
-	t.is(headers.accept, 'application/json');
+	// TODO: The response is not typed, so we need to cast as any
+	t.is((headers as any).accept, 'application/json');
 	t.is(headers['user-agent'], 'got (https://github.com/sindresorhus/got)');
 	t.is(headers['accept-encoding'], supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate');
 	t.is(headers['x-request-id'], 'value');
@@ -183,19 +184,15 @@ test('buffer as `options.body` sets `content-length` header', withServer, async
 	t.is(Number(headers['content-length']), buffer.length);
 });
 
-test('removes null value headers', withServer, async (t, server, got) => {
-	server.get('/', echoHeaders);
-
-	const {body} = await got({
+test('throws on null value headers', async t => {
+	await t.throwsAsync(got({
 		headers: {
 			'user-agent': null
 		}
-	});
-	const headers = JSON.parse(body);
-	t.false(Reflect.has(headers, 'user-agent'));
+	}), TypeError, 'Use `undefined` instead of `null` to delete HTTP headers');
 });
 
-test('setting a header to undefined keeps the old value', withServer, async (t, server, got) => {
+test('removes undefined value headers', withServer, async (t, server, got) => {
 	server.get('/', echoHeaders);
 
 	const {body} = await got({
@@ -204,7 +201,7 @@ test('setting a header to undefined keeps the old value', withServer, async (t,
 		}
 	});
 	const headers = JSON.parse(body);
-	t.not(headers['user-agent'], undefined);
+	t.is(headers['user-agent'], undefined);
 });
 
 test('non-existent headers set to undefined are omitted', withServer, async (t, server, got) => {
diff --git a/test/hooks.ts b/test/hooks.ts
index eebdab6..396d31b 100644
--- a/test/hooks.ts
+++ b/test/hooks.ts
@@ -1,4 +1,5 @@
 import test from 'ava';
+import getStream from 'get-stream';
 import delay = require('delay');
 import got from '../source';
 import withServer from './helpers/with-server';
@@ -185,8 +186,8 @@ test('init is called with options', withServer, async (t, server, got) => {
 		hooks: {
 			init: [
 				options => {
-					t.is(options.path, '/');
-					t.is(options.hostname, 'localhost');
+					t.is(options.url.pathname, '/');
+					t.is(options.url.hostname, 'localhost');
 				}
 			]
 		}
@@ -218,8 +219,8 @@ test('beforeRequest is called with options', withServer, async (t, server, got)
 		hooks: {
 			beforeRequest: [
 				options => {
-					t.is(options.path, '/');
-					t.is(options.hostname, 'localhost');
+					t.is(options.url.pathname, '/');
+					t.is(options.url.hostname, 'localhost');
 				}
 			]
 		}
@@ -251,8 +252,8 @@ test('beforeRedirect is called with options and response', withServer, async (t,
 		hooks: {
 			beforeRedirect: [
 				(options, response) => {
-					t.is(options.path, '/');
-					t.is(options.hostname, 'localhost');
+					t.is(options.url.pathname, '/');
+					t.is(options.url.hostname, 'localhost');
 
 					t.is(response.statusCode, 302);
 					t.is(new URL(response.url).pathname, '/redirect');
@@ -291,7 +292,7 @@ test('beforeRetry is called with options', withServer, async (t, server, got) =>
 		hooks: {
 			beforeRetry: [
 				(options, error, retryCount) => {
-					t.is(options.hostname, 'localhost');
+					t.is(options.url.hostname, 'localhost');
 					t.truthy(error);
 					t.true(retryCount >= 1);
 				}
@@ -381,6 +382,44 @@ test('afterResponse allows to retry', withServer, async (t, server, got) => {
 	t.is(statusCode, 200);
 });
 
+test('afterResponse allows to retry - `beforeRetry` hook', withServer, async (t, server, got) => {
+	server.get('/', (request, response) => {
+		if (request.headers.token !== 'unicorn') {
+			response.statusCode = 401;
+		}
+
+		response.end();
+	});
+
+	let called = false;
+
+	const {statusCode} = await got({
+		hooks: {
+			afterResponse: [
+				(response, retryWithMergedOptions) => {
+					if (response.statusCode === 401) {
+						return retryWithMergedOptions({
+							headers: {
+								token: 'unicorn'
+							}
+						});
+					}
+
+					return response;
+				}
+			],
+			beforeRetry: [
+				options => {
+					t.truthy(options);
+					called = true;
+				}
+			]
+		}
+	});
+	t.is(statusCode, 200);
+	t.true(called);
+});
+
 test('no infinity loop when retrying on afterResponse', withServer, async (t, server, got) => {
 	server.get('/', (request, response) => {
 		if (request.headers.token !== 'unicorn') {
@@ -474,12 +513,59 @@ test('doesn\'t throw on afterResponse retry HTTP failure if throwHttpErrors is f
 	t.is(statusCode, 500);
 });
 
-test('beforeError is called with an error', async t => {
-	await t.throwsAsync(got('https://example.com', {
-		request: () => {
-			throw error;
-		},
+test('throwing in a beforeError hook - promise', withServer, async (t, server, got) => {
+	server.get('/', (_request, response) => {
+		response.end('ok');
+	});
+
+	await t.throwsAsync(got({
+		hooks: {
+			afterResponse: [
+				() => {
+					throw error;
+				}
+			],
+			beforeError: [
+				() => {
+					throw new Error('foobar');
+				},
+				// @ts-ignore Assertion.
+				() => {
+					t.fail('This shouldn\'t be called at all');
+				}
+			]
+		}
+	}), 'foobar');
+});
+
+test('throwing in a beforeError hook - stream', withServer, async (t, _server, got) => {
+	await t.throwsAsync(getStream(got.stream({
+		hooks: {
+			beforeError: [
+				() => {
+					throw new Error('foobar');
+				},
+				// @ts-ignore Assertion.
+				() => {
+					t.fail('This shouldn\'t be called at all');
+				}
+			]
+		}
+	})), 'foobar');
+});
+
+test('beforeError is called with an error - promise', withServer, async (t, server, got) => {
+	server.get('/', (_request, response) => {
+		response.end('ok');
+	});
+
+	await t.throwsAsync(got({
 		hooks: {
+			afterResponse: [
+				() => {
+					throw error;
+				}
+			],
 			beforeError: [error2 => {
 				t.true(error2 instanceof Error);
 				return error2;
@@ -488,6 +574,17 @@ test('beforeError is called with an error', async t => {
 	}), errorString);
 });
 
+test('beforeError is called with an error - stream', withServer, async (t, _server, got) => {
+	await t.throwsAsync(getStream(got.stream({
+		hooks: {
+			beforeError: [error2 => {
+				t.true(error2 instanceof Error);
+				return error2;
+			}]
+		}
+	})), 'Response code 404 (Not Found)');
+});
+
 test('beforeError allows modifications', async t => {
 	const errorString2 = 'foobar';
 
diff --git a/test/http.ts b/test/http.ts
index 4e5ca50..f13cd6e 100644
--- a/test/http.ts
+++ b/test/http.ts
@@ -114,10 +114,10 @@ test('response contains got options', withServer, async (t, server, got) => {
 	});
 
 	const options = {
-		auth: 'foo:bar'
+		username: 'foo'
 	};
 
-	t.is((await got(options)).request.options.auth, options.auth);
+	t.is((await got(options)).request.options.username, options.username);
 });
 
 test('socket destroyed by the server throws ECONNRESET', withServer, async (t, server, got) => {
diff --git a/test/merge-instances.ts b/test/merge-instances.ts
index 88ded05..0e27612 100644
--- a/test/merge-instances.ts
+++ b/test/merge-instances.ts
@@ -20,27 +20,11 @@ test('merging instances', withServer, async (t, server) => {
 	t.not(headers['user-agent'], undefined);
 });
 
-test('works even if no default handler in the end', withServer, async (t, server) => {
-	server.get('/', echoHeaders);
-
-	const instanceA = got.create({
-		options: {}
-	});
-
-	const instanceB = got.create({
-		options: {}
-	});
-
-	const merged = instanceA.extend(instanceB);
-	await t.notThrowsAsync(merged(server.url));
-});
-
 test('merges default handlers & custom handlers', withServer, async (t, server) => {
 	server.get('/', echoHeaders);
 
 	const instanceA = got.extend({headers: {unicorn: 'rainbow'}});
-	const instanceB = got.create({
-		options: {},
+	const instanceB = got.extend({
 		handlers: [
 			(options, next) => {
 				options.headers.cat = 'meow';
@@ -115,50 +99,7 @@ test('hooks are merged', t => {
 	t.deepEqual(getBeforeRequestHooks(merged), getBeforeRequestHooks(instanceA).concat(getBeforeRequestHooks(instanceB)));
 });
 
-test('hooks are passed by though other instances don\'t have them', t => {
-	const instanceA = got.extend({hooks: {
-		beforeRequest: [
-			options => {
-				options.headers.dog = 'woof';
-			}
-		]
-	}});
-	const instanceB = got.create({
-		options: {}
-	});
-	const instanceC = got.create({
-		options: {hooks: {}}
-	});
-
-	const merged = instanceA.extend(instanceB, instanceC);
-	t.deepEqual(merged.defaults.options.hooks.beforeRequest, instanceA.defaults.options.hooks.beforeRequest);
-});
-
-test('URLSearchParams instances are merged', t => {
-	const instanceA = got.extend({
-		searchParams: new URLSearchParams({a: '1'})
-	});
-
-	const instanceB = got.extend({
-		searchParams: new URLSearchParams({b: '2'})
-	});
-
-	const merged = instanceA.extend(instanceB);
-	// @ts-ignore Manual tests
-	t.is(merged.defaults.options.searchParams.get('a'), '1');
-	// @ts-ignore Manual tests
-	t.is(merged.defaults.options.searchParams.get('b'), '2');
-});
-
-// TODO: remove this before Got v11
-test('`got.mergeInstances()` works', t => {
-	const instance = got.mergeInstances(got, got.create({
-		options: {
-			headers: {
-				'user-agent': null
-			}
-		}
-	}));
-
-	t.is(instance.defaults.options.headers['user-agent'], null);
+test('default handlers are not duplicated', t => {
+	const instance = got.extend(got);
+	t.is(instance.defaults.handlers.length, 1);
 });
diff --git a/test/options-to-url.ts b/test/options-to-url.ts
new file mode 100644
index 0000000..4219fb5
--- /dev/null
+++ b/test/options-to-url.ts
@@ -0,0 +1,122 @@
+import test from 'ava';
+import is from '@sindresorhus/is';
+import optionsToUrl from '../source/utils/options-to-url';
+
+test('`path` is deprecated', t => {
+	t.throws(() => {
+		// @ts-ignore
+		optionsToUrl({path: ''});
+	}, 'Parameter `path` is deprecated. Use `pathname` instead.');
+});
+
+test('`auth` is deprecated', t => {
+	t.throws(() => {
+		// @ts-ignore
+		optionsToUrl({auth: ''});
+	}, 'Parameter `auth` is deprecated. Use `username`/`password` instead.');
+});
+
+test('`search` and `searchParams` are mutually exclusive', t => {
+	t.throws(() => {
+		// @ts-ignore
+		optionsToUrl({search: 'a', searchParams: {}});
+	}, 'Parameters `search` and `searchParams` are mutually exclusive.');
+});
+
+test('`href` option', t => {
+	const href = 'https://google.com/';
+
+	const url = optionsToUrl({href});
+	t.is(url.href, href);
+	t.true(is.urlInstance(url));
+});
+
+test('`origin` option', t => {
+	const origin = 'https://google.com';
+
+	const url = optionsToUrl({origin});
+	t.is(url.href, `${origin}/`);
+	t.true(is.urlInstance(url));
+});
+
+test('throws if no protocol specified', t => {
+	t.throws(() => {
+		optionsToUrl({});
+	}, 'No URL protocol specified');
+});
+
+test('`port` option', t => {
+	const origin = 'https://google.com';
+
+	const url = optionsToUrl({origin, port: 8888});
+	t.is(url.href, `${origin}:8888/`);
+	t.true(is.urlInstance(url));
+});
+
+test('`protocol` option', t => {
+	const origin = 'https://google.com';
+
+	const url = optionsToUrl({origin, protocol: 'http:'});
+	t.is(url.href, 'http://google.com/');
+	t.true(is.urlInstance(url));
+});
+
+test('`username` option', t => {
+	const origin = 'https://google.com';
+
+	const url = optionsToUrl({origin, username: 'username'});
+	t.is(url.href, 'https://username@google.com/');
+	t.true(is.urlInstance(url));
+});
+
+test('`password` option', t => {
+	const origin = 'https://google.com';
+
+	const url = optionsToUrl({origin, password: 'password'});
+	t.is(url.href, 'https://:password@google.com/');
+	t.true(is.urlInstance(url));
+});
+
+test('`username` option combined with `password` option', t => {
+	const origin = 'https://google.com';
+
+	const url = optionsToUrl({origin, username: 'username', password: 'password'});
+	t.is(url.href, 'https://username:password@google.com/');
+	t.true(is.urlInstance(url));
+});
+
+test('`host` option', t => {
+	const url = optionsToUrl({protocol: 'https:', host: 'google.com'});
+	t.is(url.href, 'https://google.com/');
+	t.true(is.urlInstance(url));
+});
+
+test('`hostname` option', t => {
+	const url = optionsToUrl({protocol: 'https:', hostname: 'google.com'});
+	t.is(url.href, 'https://google.com/');
+	t.true(is.urlInstance(url));
+});
+
+test('`pathname` option', t => {
+	const url = optionsToUrl({protocol: 'https:', hostname: 'google.com', pathname: '/foobar'});
+	t.is(url.href, 'https://google.com/foobar');
+	t.true(is.urlInstance(url));
+});
+
+test('`search` option', t => {
+	const url = optionsToUrl({protocol: 'https:', hostname: 'google.com', search: '?a=1'});
+	t.is(url.href, 'https://google.com/?a=1');
+	t.true(is.urlInstance(url));
+});
+
+test('`hash` option', t => {
+	const url = optionsToUrl({protocol: 'https:', hostname: 'google.com', hash: 'foobar'});
+	t.is(url.href, 'https://google.com/#foobar');
+	t.true(is.urlInstance(url));
+});
+
+test('merges provided `searchParams`', t => {
+	const url = optionsToUrl({origin: 'https://google.com/?a=1', searchParams: {b: 2}});
+	t.is(url.href, 'https://google.com/?a=1&b=2');
+	t.true(is.urlInstance(url));
+});
diff --git a/test/post.ts b/test/post.ts
index 17e5d13..1c42e38 100644
--- a/test/post.ts
+++ b/test/post.ts
@@ -22,6 +22,14 @@ test('GET cannot have body', withServer, async (t, server, got) => {
 	await t.throwsAsync(got.get({body: 'hi'}), 'The `GET` method cannot be used with a body');
 });
 
+test('invalid body', async t => {
+	await t.throwsAsync(
+		got('https://example.com', {body: {} as any}),
+		TypeError,
+		'The `body` option must be a stream.Readable, string or Buffer'
+	);
+});
+
 test('sends strings', withServer, async (t, server, got) => {
 	server.post('/', defaultEndpoint);
 
diff --git a/test/promise.ts b/test/promise.ts
index 8269466..f5a3c8c 100644
--- a/test/promise.ts
+++ b/test/promise.ts
@@ -6,7 +6,7 @@ import withServer from './helpers/with-server';
 test('emits request event as promise', withServer, async (t, server, got) => {
 	server.get('/', (_request, response) => {
 		response.statusCode = 200;
-		response.end();
+		response.end('null');
 	});
 
 	await got('').json().on('request', request => {
@@ -17,7 +17,7 @@ test('emits request event as promise', withServer, async (t, server, got) => {
 test('emits response event as promise', withServer, async (t, server, got) => {
 	server.get('/', (_request, response) => {
 		response.statusCode = 200;
-		response.end();
+		response.end('null');
 	});
 
 	await got('').json().on('response', response => {
diff --git a/test/query.ts b/test/query.ts
deleted file mode 100644
index 786ff6e..0000000
--- a/test/query.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import test from 'ava';
-import withServer from './helpers/with-server';
-
-// TODO: Remove this file before the Got v11 release together with completely removing the `query` option
-
-const echoUrl = (request, response) => {
-	response.end(request.url);
-};
-
-test('overrides query from options', withServer, async (t, server, got) => {
-	server.get('/', echoUrl);
-
-	const {body} = await got(
-		'?drop=this',
-		{
-			query: {
-				test: 'wow'
-			},
-			cache: {
-				get(key) {
-					t.is(key, `cacheable-request:GET:${server.url}/?test=wow`);
-				},
-				set(key) {
-					t.is(key, `cacheable-request:GET:${server.url}/?test=wow`);
-				}
-			}
-		}
-	);
-
-	t.is(body, '/?test=wow');
-});
-
-test('escapes query parameter values', withServer, async (t, server, got) => {
-	server.get('/', echoUrl);
-
-	const {body} = await got({
-		query: {
-			test: 'it’s ok'
-		}
-	});
-
-	t.is(body, '/?test=it%E2%80%99s+ok');
-});
-
-test('the `query` option can be a URLSearchParams', withServer, async (t, server, got) => {
-	server.get('/', echoUrl);
-
-	const query = new URLSearchParams({test: 'wow'});
-	const {body} = await got({query});
-	t.is(body, '/?test=wow');
-});
-
-test('should ignore empty query object', withServer, async (t, server, got) => {
-	server.get('/', echoUrl);
-
-	t.is((await got({query: {}})).requestUrl, `${server.url}/`);
-});
-
-test('query option', withServer, async (t, server, got) => {
-	server.get('/', (request, response) => {
-		t.is(request.query.recent, 'true');
-		response.end('recent');
-	});
-
-	t.is((await got({query: {recent: true}})).body, 'recent');
-	t.is((await got({query: 'recent=true'})).body, 'recent');
-});
-
-test('query in options are not breaking redirects', withServer, async (t, server, got) => {
-	server.get('/', (_request, response) => {
-		response.end('reached');
-	});
-
-	server.get('/relativeQuery', (request, response) => {
-		t.is(request.query.bang, '1');
-
-		response.writeHead(302, {
-			location: '/'
-		});
-		response.end();
-	});
-
-	t.is((await got('relativeQuery', {query: 'bang=1'})).body, 'reached');
-});
diff --git a/test/redirects.ts b/test/redirects.ts
index 0146b75..3142a02 100644
--- a/test/redirects.ts
+++ b/test/redirects.ts
@@ -4,7 +4,12 @@ import nock = require('nock');
 import withServer from './helpers/with-server';
 
 const reachedHandler = (_request, response) => {
-	response.end('reached');
+	const body = 'reached';
+
+	response.writeHead(200, {
+		'content-length': body.length
+	});
+	response.end(body);
 };
 
 const finiteHandler = (_request, response) => {
@@ -110,13 +115,13 @@ test('searchParams are not breaking redirects', withServer, async (t, server, go
 	t.is((await got('relativeSearchParam', {searchParams: 'bang=1'})).body, 'reached');
 });
 
-test('hostname + path are not breaking redirects', withServer, async (t, server, got) => {
+test('hostname + pathname are not breaking redirects', withServer, async (t, server, got) => {
 	server.get('/', reachedHandler);
 	server.get('/relative', relativeHandler);
 
 	t.is((await got('relative', {
 		hostname: server.hostname,
-		path: '/relative'
+		pathname: '/relative'
 	})).body, 'reached');
 });
 
@@ -146,6 +151,22 @@ test('redirects POST requests', withServer, async (t, server, got) => {
 	});
 });
 
+test('redirects on 303 if GET or HEAD', withServer, async (t, server, got) => {
+	server.get('/', reachedHandler);
+
+	server.head('/seeOther', (_request, response) => {
+		response.writeHead(303, {
+			location: '/'
+		});
+		response.end();
+	});
+
+	const {url, headers, request} = await got.head('seeOther');
+	t.is(url, `${server.url}/`);
+	t.is(headers['content-length'], 'reached'.length.toString());
+	t.is(request.options.method, 'HEAD');
+});
+
 test('redirects on 303 response even on post, put, delete', withServer, async (t, server, got) => {
 	server.get('/', reachedHandler);
 
diff --git a/test/response-parse.ts b/test/response-parse.ts
index 5b53066..3fcf589 100644
--- a/test/response-parse.ts
+++ b/test/response-parse.ts
@@ -14,6 +14,15 @@ test('`options.resolveBodyOnly` works', withServer, async (t, server, got) => {
 	t.deepEqual(await got({responseType: 'json', resolveBodyOnly: true}), dog);
 });
 
+test('`options.resolveBodyOnly` combined with `options.throwHttpErrors`', withServer, async (t, server, got) => {
+	server.get('/', (_request, response) => {
+		response.statusCode = 404;
+		response.end('/');
+	});
+
+	t.is(await got({resolveBodyOnly: true, throwHttpErrors: false}), '/');
+});
+
 test('JSON response', withServer, async (t, server, got) => {
 	server.get('/', defaultHandler);
 
@@ -50,23 +59,28 @@ test('Text response - promise.text()', withServer, async (t, server, got) => {
 	t.is(await got('').text(), jsonResponse);
 });
 
-test('throws an error on invalid response type', withServer, async (t, server, got) => {
+test('Text response - promise.json().text()', withServer, async (t, server, got) => {
 	server.get('/', defaultHandler);
 
-	const error = await t.throwsAsync(got({responseType: 'invalid'}), /^Failed to parse body of type 'invalid'/);
-	// @ts-ignore
-	t.true(error.message.includes(error.options.hostname));
-	// @ts-ignore
-	t.is(error.options.path, '/');
+	t.is(await got('').json().text(), jsonResponse);
 });
 
-test('doesn\'t parse responses without a body', withServer, async (t, server, got) => {
-	server.get('/', (_request, response) => {
-		response.end();
-	});
+test('works if promise has been already resolved', withServer, async (t, server, got) => {
+	server.get('/', defaultHandler);
 
-	const body = await got('').json();
-	t.is(body, '');
+	const promise = got('').text();
+	t.is(await promise, jsonResponse);
+	t.deepEqual(await promise.json(), dog);
+});
+
+test('throws an error on invalid response type', withServer, async (t, server, got) => {
+	server.get('/', defaultHandler);
+
+	const error = await t.throwsAsync(got({responseType: 'invalid'}), /^Failed to parse body of type 'string' as 'invalid'/);
+	// @ts-ignore
+	t.true(error.message.includes(error.options.url.hostname));
+	// @ts-ignore
+	t.is(error.options.url.pathname, '/');
 });
 
 test('wraps parsing errors', withServer, async (t, server, got) => {
@@ -76,9 +90,9 @@ test('wraps parsing errors', withServer, async (t, server, got) => {
 
 	const error = await t.throwsAsync(got({responseType: 'json'}), got.ParseError);
 	// @ts-ignore
-	t.true(error.message.includes(error.options.hostname));
+	t.true(error.message.includes(error.options.url.hostname));
 	// @ts-ignore
-	t.is(error.options.path, '/');
+	t.is(error.options.url.pathname, '/');
 });
 
 test('parses non-200 responses', withServer, async (t, server, got) => {
@@ -106,7 +120,7 @@ test('ignores errors on invalid non-200 responses', withServer, async (t, server
 	// @ts-ignore
 	t.is(error.response.body, 'Internal error');
 	// @ts-ignore
-	t.is(error.options.path, '/');
+	t.is(error.options.url.pathname, '/');
 });
 
 test('parse errors have `response` property', withServer, async (t, server, got) => {
@@ -129,3 +143,13 @@ test('sets correct headers', withServer, async (t, server, got) => {
 	t.is(headers['content-type'], 'application/json');
 	t.is(headers.accept, 'application/json');
 });
+
+test('doesn\'t throw on 204 No Content', withServer, async (t, server, got) => {
+	server.get('/', (_request, response) => {
+		response.statusCode = 204;
+		response.end();
+	});
+
+	const body = await got('').json();
+	t.is(body, '');
+});
diff --git a/test/retry.ts b/test/retry.ts
index 9d4103f..c074db4 100644
--- a/test/retry.ts
+++ b/test/retry.ts
@@ -178,7 +178,7 @@ test('respects 413 Retry-After', withServer, async (t, server, got) => {
 });
 
 test('respects 413 Retry-After with RFC-1123 timestamp', withServer, async (t, server, got) => {
-	let lastTried413TimestampAccess;
+	let lastTried413TimestampAccess: string;
 	server.get('/', (_request, response) => {
 		const date = (new Date(Date.now() + (retryAfterOn413 * 1000))).toUTCString();
 
@@ -327,3 +327,23 @@ test('does not retry on POST', withServer, async (t, server, got) => {
 		}
 	}), got.TimeoutError);
 });
+
+test('does not break on redirect', withServer, async (t, server, got) => {
+	server.get('/', (_request, response) => {
+		response.statusCode = 500;
+		response.end();
+	});
+
+	let tries = 0;
+	server.get('/redirect', (_request, response) => {
+		tries++;
+
+		response.writeHead(302, {
+			location: '/'
+		});
+		response.end();
+	});
+
+	await t.throwsAsync(got('redirect'), 'Response code 500 (Internal Server Error)');
+	t.is(tries, 1);
+});
diff --git a/test/stream.ts b/test/stream.ts
index 88add5b..d3e4094 100644
--- a/test/stream.ts
+++ b/test/stream.ts
@@ -72,6 +72,14 @@ test('throws on write if body is specified', withServer, (t, server, got) => {
 	}, 'Got\'s stream is not writable when the `body` option is used');
 });
 
+test('throws on write if no payload method is present', withServer, (t, server, got) => {
+	server.post('/', postHandler);
+
+	t.throws(() => {
+		got.stream.get('').end('wow');
+	}, 'The `GET` method cannot be used with a body');
+});
+
 test('has request event', withServer, async (t, server, got) => {
 	server.get('/', defaultHandler);
 
@@ -113,7 +121,7 @@ test('has error event', withServer, async (t, server, got) => {
 });
 
 test('has error event #2', withServer, async (t, _server, got) => {
-	const stream = got.stream('http://doesntexist', {prefixUrl: null});
+	const stream = got.stream('http://doesntexist', {prefixUrl: ''});
 	await t.throwsAsync(pEvent(stream, 'response'), {code: 'ENOTFOUND'});
 });