diff --git a/advanced-creation.md b/advanced-creation.md
deleted file mode 100644
index ef75966a2..000000000
--- a/advanced-creation.md
+++ /dev/null
@@ -1,270 +0,0 @@
-# Advanced creation
-
-> 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)
-
-Configure 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()`, this method has no defaults.
-
-##### [options](readme.md#options)
-
-To inherit from parent, set it as `got.defaults.options` or use [`got.mergeOptions(defaults.options, options)`](readme.md#gotmergeoptionsparentoptions-newoptions).
-**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`
-Default: `false`
-
-States if the defaults are mutable. It's very useful when you need to [update headers over time](readme.md#hooksafterresponse).
-
-##### handler
-
-Type: `Function`
-Default: `undefined`
-
-A function making additional changes to the request.
-
-To inherit from parent, set it as `got.defaults.handler`.
-To use the default handler, just omit specifying this.
-
-###### [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 = {
- handler: (options, next) => {
- if (options.stream) {
- // It's a Stream
- // 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);
-```
-
-```js
-const defaults = {
- options: {
- method: 'GET',
- retry: {
- retries: 2,
- methods: [
- 'GET',
- 'PUT',
- 'HEAD',
- 'DELETE',
- 'OPTIONS',
- 'TRACE'
- ],
- statusCodes: [
- 408,
- 413,
- 429,
- 500,
- 502,
- 503,
- 504
- ],
- errorCodes: [
- 'ETIMEDOUT',
- 'ECONNRESET',
- 'EADDRINUSE',
- 'ECONNREFUSED',
- 'EPIPE',
- 'ENOTFOUND',
- 'ENETUNREACH',
- 'EAI_AGAIN'
- ]
- },
- headers: {
- 'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`
- },
- hooks: {
- beforeError: [],
- init: [],
- beforeRequest: [],
- beforeRedirect: [],
- beforeRetry: [],
- afterResponse: []
- },
- decompress: true,
- throwHttpErrors: true,
- followRedirect: true,
- stream: false,
- form: false,
- cache: false,
- useElectronNet: false,
- responseType: 'text',
- resolveBodyOnly: 'false'
- },
- mutableDefaults: false
-};
-
-// Same as:
-const defaults = {
- handler: got.defaults.handler,
- options: got.defaults.options,
- mutableDefaults: got.defaults.mutableDefaults
-};
-
-const unchangedGot = got.create(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'}});
-```
-
-### 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.
-
-#### got.mergeInstances(instanceA, instanceB, ...)
-
-Merges many instances into a single one:
-- options are merged using [`got.mergeOptions()`](readme.md#gotmergeoptionsparentoptions-newoptions) (+ hooks are merged too),
-- handlers are stored in an array.
-
-## Examples
-
-Some examples of what kind of instances you could compose together:
-
-#### Denying redirects that lead to other sites than specified
-
-```js
-const controlRedirects = got.create({
- options: got.defaults.options,
- handler: (options, next) => {
- const promiseOrStream = next(options);
- return promiseOrStream.on('redirect', resp => {
- const host = new URL(resp.url).host;
- if (options.allowedHosts && !options.allowedHosts.includes(host)) {
- promiseOrStream.cancel(`Redirection to ${host} is not allowed`);
- }
- });
- }
-});
-```
-
-#### Limiting download & upload
-
-It's very useful in case your machine's got a little amount of RAM.
-
-```js
-const limitDownloadUpload = got.create({
- options: got.defaults.options,
- handler: (options, next) => {
- let promiseOrStream = next(options);
- if (typeof options.downloadLimit === 'number') {
- promiseOrStream.on('downloadProgress', progress => {
- if (progress.transferred > options.downloadLimit && progress.percent !== 1) {
- promiseOrStream.cancel(`Exceeded the download limit of ${options.downloadLimit} bytes`);
- }
- });
- }
-
- if (typeof options.uploadLimit === 'number') {
- promiseOrStream.on('uploadProgress', progress => {
- if (progress.transferred > options.uploadLimit && progress.percent !== 1) {
- promiseOrStream.cancel(`Exceeded the upload limit of ${options.uploadLimit} bytes`);
- }
- });
- }
-
- return promiseOrStream;
- }
-});
-```
-
-#### No user agent
-
-```js
-const noUserAgent = got.extend({
- headers: {
- 'user-agent': null
- }
-});
-```
-
-#### Custom endpoint
-
-```js
-const httpbin = got.extend({
- prefixUrl: 'https://httpbin.org/'
-});
-```
-
-#### Signing requests
-
-```js
-const crypto = require('crypto');
-const getMessageSignature = (data, secret) => crypto.createHmac('sha256', secret).update(data).digest('hex').toUpperCase();
-const signRequest = got.extend({
- hooks: {
- beforeRequest: [
- options => {
- options.headers['sign'] = getMessageSignature(options.body || '', process.env.SECRET);
- }
- ]
- }
-});
-```
-
-#### Putting it all together
-
-If these instances are different modules and you don't want to rewrite them, use `got.mergeInstances()`.
-
-**Note**: The `noUserAgent` instance must be placed at the end of chain as the instances are merged in order. Other instances do have the `user-agent` header.
-
-```js
-const merged = got.mergeInstances(controlRedirects, limitDownloadUpload, httpbin, signRequest, noUserAgent);
-
-(async () => {
- // There's no 'user-agent' header :)
- await merged('/');
- /* HTTP Request =>
- * GET / HTTP/1.1
- * accept-encoding: gzip, deflate, br
- * sign: F9E66E179B6747AE54108F82F8ADE8B3C25D76FD30AFDE6C395822C530196169
- * Host: httpbin.org
- * Connection: close
- */
-
- const MEGABYTE = 1048576;
- await merged('http://ipv4.download.thinkbroadband.com/5MB.zip', {downloadLimit: MEGABYTE, prefixUrl: ''});
- // CancelError: Exceeded the download limit of 1048576 bytes
-
- await merged('https://jigsaw.w3.org/HTTP/300/301.html', {allowedHosts: ['google.com'], prefixUrl: ''});
- // CancelError: Redirection to jigsaw.w3.org is not allowed
-})();
-```
diff --git a/documentation/advanced-creation.md b/documentation/advanced-creation.md
new file mode 100644
index 000000000..f37f60099
--- /dev/null
+++ b/documentation/advanced-creation.md
@@ -0,0 +1,241 @@
+# Advanced creation
+
+> 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).
+**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.
+
+**Note #2:** [`got.mergeOptions()`](../readme.md#gotmergeoptionsparentoptions-newoptions) does not merge hooks. Use [`got.extend()`](../readme.md#gotextendinstances) instead.
+
+##### mutableDefaults
+
+Type: `boolean`
+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[]`
+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`.
+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.
+
+To mix them use `instanceA.extend(instanceB, instanceC, ...)`, that's all.
+
+## Examples
+
+Some examples of what kind of instances you could compose together:
+
+#### Denying redirects that lead to other sites than specified
+
+```js
+const controlRedirects = got.extend({
+ handlers: [
+ (options, next) => {
+ const promiseOrStream = next(options);
+ return promiseOrStream.on('redirect', response => {
+ const host = new URL(resp.url).host;
+ if (options.allowedHosts && !options.allowedHosts.includes(host)) {
+ promiseOrStream.cancel(`Redirection to ${host} is not allowed`);
+ }
+ });
+ }
+ ]
+});
+```
+
+#### Limiting download & upload size
+
+It can be useful when your machine has limited amount of memory.
+
+```js
+const limitDownloadUpload = got.extend({
+ handlers: [
+ (options, next) => {
+ let promiseOrStream = next(options);
+ if (typeof options.downloadLimit === 'number') {
+ promiseOrStream.on('downloadProgress', progress => {
+ if (progress.transferred > options.downloadLimit && progress.percent !== 1) {
+ promiseOrStream.cancel(`Exceeded the download limit of ${options.downloadLimit} bytes`);
+ }
+ });
+ }
+
+ if (typeof options.uploadLimit === 'number') {
+ promiseOrStream.on('uploadProgress', progress => {
+ if (progress.transferred > options.uploadLimit && progress.percent !== 1) {
+ promiseOrStream.cancel(`Exceeded the upload limit of ${options.uploadLimit} bytes`);
+ }
+ });
+ }
+
+ return promiseOrStream;
+ }
+ ]
+});
+```
+
+#### No user agent
+
+```js
+const noUserAgent = got.extend({
+ headers: {
+ 'user-agent': null
+ }
+});
+```
+
+#### Custom endpoint
+
+```js
+const httpbin = got.extend({
+ prefixUrl: 'https://httpbin.org/'
+});
+```
+
+#### Signing requests
+
+```js
+const crypto = require('crypto');
+
+const getMessageSignature = (data, secret) => crypto.createHmac('sha256', secret).update(data).digest('hex').toUpperCase();
+const signRequest = got.extend({
+ hooks: {
+ beforeRequest: [
+ options => {
+ options.headers['sign'] = getMessageSignature(options.body || '', process.env.SECRET);
+ }
+ ]
+ }
+});
+```
+
+#### Putting it all together
+
+If these instances are different modules and you don't want to rewrite them, use `got.extend(...instances)`.
+
+**Note**: The `noUserAgent` instance must be placed at the end of chain as the instances are merged in order. Other instances do have the `user-agent` header.
+
+```js
+const merged = got.extend(controlRedirects, limitDownloadUpload, httpbin, signRequest, noUserAgent);
+
+(async () => {
+ // There's no 'user-agent' header :)
+ await merged('/');
+ /* HTTP Request =>
+ * GET / HTTP/1.1
+ * accept-encoding: gzip, deflate, br
+ * sign: F9E66E179B6747AE54108F82F8ADE8B3C25D76FD30AFDE6C395822C530196169
+ * Host: httpbin.org
+ * Connection: close
+ */
+
+ const MEGABYTE = 1048576;
+ await merged('http://ipv4.download.thinkbroadband.com/5MB.zip', {downloadLimit: MEGABYTE, prefixUrl: ''});
+ // CancelError: Exceeded the download limit of 1048576 bytes
+
+ await merged('https://jigsaw.w3.org/HTTP/300/301.html', {allowedHosts: ['google.com'], prefixUrl: ''});
+ // CancelError: Redirection to jigsaw.w3.org is not allowed
+})();
+```
diff --git a/documentation/examples/gh-got.js b/documentation/examples/gh-got.js
new file mode 100644
index 000000000..a8d461858
--- /dev/null
+++ b/documentation/examples/gh-got.js
@@ -0,0 +1,61 @@
+'use strict';
+const got = require('../..');
+const package = require('../../package');
+
+const getRateLimit = ({headers}) => ({
+ limit: parseInt(headers['x-ratelimit-limit'], 10),
+ remaining: parseInt(headers['x-ratelimit-remaining'], 10),
+ reset: new Date(parseInt(headers['x-ratelimit-reset'], 10) * 1000)
+});
+
+const instance = got.extend({
+ baseUrl: 'https://api.github.com',
+ headers: {
+ accept: 'application/vnd.github.v3+json',
+ 'user-agent': `${package.name}/${package.version}`
+ },
+ responseType: 'json',
+ token: process.env.GITHUB_TOKEN,
+ handlers: [
+ (options, next) => {
+ // Authorization
+ if (options.token && !options.headers.authorization) {
+ options.headers.authorization = `token ${options.token}`;
+ }
+
+ // Don't touch streams
+ if (options.stream) {
+ return next(options);
+ }
+
+ // Magic begins
+ return (async () => {
+ try {
+ const response = await next(options);
+
+ // Rate limit for the Response object
+ response.rateLimit = getRateLimit(response.headers);
+
+ return response;
+ } catch (error) {
+ const {response} = error;
+
+ // Nicer errors
+ if (response && response.body) {
+ error.name = 'GitHubError';
+ error.message = `${response.body.message} (${error.statusCode} status code)`;
+ }
+
+ // Rate limit for errors
+ if (response) {
+ error.rateLimit = getRateLimit(response.headers);
+ }
+
+ throw error;
+ }
+ })();
+ }
+ ]
+});
+
+module.exports = instance;
diff --git a/documentation/lets-make-a-plugin.md b/documentation/lets-make-a-plugin.md
new file mode 100644
index 000000000..5b3171e5f
--- /dev/null
+++ b/documentation/lets-make-a-plugin.md
@@ -0,0 +1,264 @@
+# Let's make a plugin!
+
+> Another example on how to use Got like a boss :electric_plug:
+
+Okay, so you already have learned some basics. That's great!
+
+When it comes to advanced usage, custom instances are really helpful.
+For example, take a look at [`gh-got`](https://github.com/sindresorhus/gh-got).
+It looks pretty complicated, but... it's really not.
+
+Before we start, we need to find the [GitHub API docs](https://developer.github.com/v3/).
+
+Let's write down the most important information:
+1. The root endpoint is `https://api.github.com/`.
+2. We will use version 3 of the API.
+ The `Accept` header needs to be set to `application/vnd.github.v3+json`.
+3. The body is in a JSON format.
+4. We will use OAuth2 for authorization.
+5. We may receive `400 Bad Request` or `422 Unprocessable Entity`.
+ The body contains detailed information about the error.
+6. *Pagination?* Not yet. This is going to be a native feature of Got. We'll update this page accordingly when the feature is available.
+7. Rate limiting. These headers are interesting:
+
+- `X-RateLimit-Limit`
+- `X-RateLimit-Remaining`
+- `X-RateLimit-Reset`
+- `X-GitHub-Request-Id`
+
+Also `X-GitHub-Request-Id` may be useful.
+
+8. User-Agent is required.
+
+When we have all the necessary info, we can start mixing :cake:
+
+### The root endpoint
+
+Not much to do here, just extend an instance and provide the `baseUrl` option:
+
+```js
+const got = require('got');
+
+const instance = got.extend({
+ baseUrl: 'https://api.github.com'
+});
+
+module.exports = instance;
+```
+
+### v3 API
+
+GitHub needs to know which version we are using. We'll use the `Accept` header for that:
+
+```js
+const got = require('got');
+
+const instance = got.extend({
+ baseUrl: 'https://api.github.com',
+ headers: {
+ accept: 'application/vnd.github.v3+json'
+ }
+});
+```
+
+### JSON body
+
+We'll use [`options.responseType`](../readme.md#responsetype):
+
+```js
+const got = require('got');
+
+const instance = got.extend({
+ baseUrl: 'https://api.github.com',
+ headers: {
+ accept: 'application/vnd.github.v3+json'
+ },
+ responseType: 'json'
+});
+
+module.exports = instance;
+```
+
+### Authorization
+
+It's common to set some environment variables, for example, `GITHUB_TOKEN`. You can modify the tokens in all your apps easily, right? Cool. What about... we want to provide a unique token for each app. Then we will need to create a new option - it will default to the environment variable, but you can easily override it.
+
+Let's use handlers instead of hooks. This will make our code more readable: having `beforeRequest`, `beforeError` and `afterResponse` hooks for just a few lines of code would complicate things unnecessarily.
+
+**Tip:** it's a good practice to use hooks when your plugin gets complicated. Try not to overload the handler function, but don't abuse hooks either.
+
+```js
+const got = require('got');
+
+const instance = got.extend({
+ baseUrl: 'https://api.github.com',
+ headers: {
+ accept: 'application/vnd.github.v3+json'
+ },
+ responseType: 'json',
+ token: process.env.GITHUB_TOKEN,
+ handlers: [
+ (options, next) => {
+ // Authorization
+ if (options.token && !options.headers.authorization) {
+ options.headers.authorization = `token ${options.token}`;
+ }
+
+ return next(options);
+ }
+ ]
+});
+
+module.exports = instance;
+```
+
+### Errors
+
+We should name our errors, just to know if the error is from the API response. Superb errors, here we come!
+
+```js
+...
+ handlers: [
+ (options, next) => {
+ // Authorization
+ if (options.token && !options.headers.authorization) {
+ options.headers.authorization = `token ${options.token}`;
+ }
+
+ // Don't touch streams
+ if (options.stream) {
+ return next(options);
+ }
+
+ // Magic begins
+ return (async () => {
+ try {
+ const response = await next(options);
+
+ return response;
+ } catch (error) {
+ const {response} = error;
+
+ // Nicer errors
+ if (response && response.body) {
+ error.name = 'GitHubError';
+ error.message = `${response.body.message} (${error.statusCode} status code)`;
+ }
+
+ throw error;
+ }
+ })();
+ }
+ ]
+...
+```
+
+### Rate limiting
+
+Umm... `response.headers['x-ratelimit-remaining']` doesn't look good. What about `response.rateLimit.limit` instead?
+Yeah, definitely. Since `response.headers` is an object, we can easily parse these:
+
+```js
+const getRateLimit = ({headers}) => ({
+ limit: parseInt(headers['x-ratelimit-limit'], 10),
+ remaining: parseInt(headers['x-ratelimit-remaining'], 10),
+ reset: new Date(parseInt(headers['x-ratelimit-reset'], 10) * 1000)
+});
+
+getRateLimit({
+ 'x-ratelimit-limit': '60',
+ 'x-ratelimit-remaining': '55',
+ 'x-ratelimit-reset': '1562852139'
+});
+// => {
+// limit: 60,
+// remaining: 55,
+// reset: 2019-07-11T13:35:39.000Z
+// }
+```
+
+Let's integrate it:
+
+```js
+const getRateLimit = ({headers}) => ({
+ limit: parseInt(headers['x-ratelimit-limit'], 10),
+ remaining: parseInt(headers['x-ratelimit-remaining'], 10),
+ reset: new Date(parseInt(headers['x-ratelimit-reset'], 10) * 1000)
+});
+
+...
+ handlers: [
+ (options, next) => {
+ // Authorization
+ if (options.token && !options.headers.authorization) {
+ options.headers.authorization = `token ${options.token}`;
+ }
+
+ // Don't touch streams
+ if (options.stream) {
+ return next(options);
+ }
+
+ // Magic begins
+ return (async () => {
+ try {
+ const response = await next(options);
+
+ // Rate limit for the Response object
+ response.rateLimit = getRateLimit(response.headers);
+
+ return response;
+ } catch (error) {
+ const {response} = error;
+
+ // Nicer errors
+ if (response && response.body) {
+ error.name = 'GitHubError';
+ error.message = `${response.body.message} (${error.statusCode} status code)`;
+ }
+
+ // Rate limit for errors
+ if (response) {
+ error.rateLimit = getRateLimit(response.headers);
+ }
+
+ throw error;
+ }
+ })();
+ }
+ ]
+...
+```
+
+### The frosting on the cake: `User-Agent` header.
+
+```js
+const package = require('./package');
+
+const instance = got.extend({
+ ...
+ headers: {
+ accept: 'application/vnd.github.v3+json',
+ 'user-agent': `${package.name}/${package.version}`
+ }
+ ...
+});
+```
+
+## Woah. Is that it?
+
+Yup. View the full source code [here](examples/gh-got.js). Here's an example of how to use it:
+
+```js
+const ghGot = require('gh-got');
+
+(async () => {
+ const response = await ghGot('users/sindresorhus');
+ const creationDate = new Date(response.created_at);
+
+ console.log(`Sindre's GitHub profile was created on ${creationDate.toGMTString()}`);
+ // => Sindre's GitHub profile was created on Sun, 20 Dec 2009 22:57:02 GMT
+})();
+```
+
+Did you know you can mix many instances into a bigger, more powerful one? Check out the [Advanced Creation](advanced-creation.md) guide.
diff --git a/migration-guides.md b/documentation/migration-guides.md
similarity index 95%
rename from migration-guides.md
rename to documentation/migration-guides.md
index b8e88796a..cf2983f82 100644
--- a/migration-guides.md
+++ b/documentation/migration-guides.md
@@ -12,9 +12,9 @@ Let's take the very first example from Request's readme:
const request = require('request');
request('https://google.com', (error, response, body) => {
- console.log('error:', error); // Print the error if one occurred
- console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
- console.log('body:', body); // Print the HTML for the Google homepage
+ console.log('error:', error);
+ console.log('statusCode:', response && response.statusCode);
+ console.log('body:', body);
});
```
diff --git a/package.json b/package.json
index 6c8091400..d63f7a333 100644
--- a/package.json
+++ b/package.json
@@ -122,6 +122,9 @@
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/await-thenable": "off"
- }
+ },
+ "ignores": [
+ "documentation/examples/*"
+ ]
}
}
diff --git a/readme.md b/readme.md
index 619c5c0c2..f98a435f2 100644
--- a/readme.md
+++ b/readme.md
@@ -58,12 +58,13 @@ Got is for Node.js. For browsers, we recommend [Ky](https://github.com/sindresor
- [WHATWG URL support](#url)
- [Hooks](#hooks)
- [Instances with custom defaults](#instances)
-- [Composable](advanced-creation.md#merging-instances)
+- [Composable](documentation/advanced-creation.md#merging-instances)
+- [Plugins](documentation/lets-make-a-plugin.md)
- [Electron support](#useelectronnet)
- [Used by ~2000 packages and ~500K repos](https://github.com/sindresorhus/got/network/dependents)
- Actively maintained
-[Moving from Request?](migration-guides.md)
+[Moving from Request?](documentation/migration-guides.md)
[See how Got compares to other HTTP libraries](#comparison)
@@ -458,9 +459,9 @@ Hooks allow modifications during the request lifecycle. Hook functions may be as
Type: `Function[]`
Default: `[]`
-Called with plain [request options](#options), right before their normalization. This is especially useful in conjunction with [`got.extend()`](#instances) and [`got.create()`](advanced-creation.md) when the input needs custom handling.
+Called with plain [request options](#options), right before their normalization. This is especially useful in conjunction with [`got.extend()`](#instances) and [`got.create()`](documentation/advanced-creation.md) when the input needs custom handling.
-See the [Request migration guide](migration-guides.md#breaking-changes) for an example.
+See the [Request migration guide](documentation/migration-guides.md#breaking-changes) for an example.
**Note:** This hook must be synchronous!
@@ -469,7 +470,7 @@ See the [Request migration guide](migration-guides.md#breaking-changes) for an e
Type: `Function[]`
Default: `[]`
-Called with [normalized](source/normalize-arguments.ts) [request options](#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()`](#instances) and [`got.create()`](advanced-creation.md) when you want to create an API client that, for example, uses HMAC-signing.
+Called with [normalized](source/normalize-arguments.ts) [request options](#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()`](#instances) and [`got.create()`](documentation/advanced-creation.md) when you want to create an API client that, for example, uses HMAC-signing.
See the [AWS section](#aws) for an example.
@@ -735,7 +736,7 @@ Sets `options.method` to the method name and makes a request.
### Instances
-#### got.extend([options])
+#### got.extend(...options)
Configure a new `got` instance with default `options`. The `options` are merged with the parent instance's `defaults.options` using [`got.mergeOptions`](#gotmergeoptionsparentoptions-newoptions). You can access the resolved options with the `.defaults` property on the instance.
@@ -780,7 +781,51 @@ client.get('/demo');
})();
```
-**Tip:** Need more control over the behavior of Got? Check out the [`got.create()`](advanced-creation.md).
+**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
+// You can now modify `mutableGot.defaults.options`.
+const mutableGot = got.extend({mutableDefaults: true});
+
+const mergedHandlers = got.extend({
+ handlers: [
+ (options, next) => {
+ delete options.headers.referer;
+
+ return next(options);
+ }
+ ]
+});
+```
+
+#### got.extend(...instances)
+
+Merges many instances into a single one:
+- options are merged using [`got.mergeOptions()`](#gotmergeoptionsparentoptions-newoptions) (+ hooks are merged too),
+- handlers are stored in an array (you can access them through `instance.defaults.handlers`).
+
+#### got.extend(...options, ...instances, ...)
+
+It's possible to combine options and instances.
+It gives the same effect as `got.extend(...options).extend(...instances)`:
+
+```js
+const a = {headers: {cat: 'meow'}};
+const b = got.create({
+ options: {
+ headers: {
+ cow: 'moo'
+ }
+ }
+});
+
+// The same as `got.extend(a).extend(b)`.
+// Note `a` is options and `b` is an instance.
+got.extend(a, b);
+//=> {headers: {cat: 'meow', cow: 'moo'}}
+```
#### got.mergeOptions(parentOptions, newOptions)
@@ -809,7 +854,7 @@ Options are deeply merged to a new object. The value of each key is determined a
Type: `object`
-The default Got options.
+The default Got options used in that instance.
## Errors
@@ -1181,7 +1226,7 @@ Bear in mind; if you send an `if-modified-since` header and receive a `304 Not M
Use `got.extend()` to make it nicer to work with REST APIs. Especially if you use the `prefixUrl` option.
-**Note:** Not to be confused with [`got.create()`](advanced-creation.md), which has no defaults.
+**Note:** Not to be confused with [`got.create()`](documentation/advanced-creation.md), which has no defaults.
```js
const got = require('got');
@@ -1201,8 +1246,6 @@ const custom = got.extend({
})();
```
-**Tip:** Need to merge some instances into a single one? Check out [`got.mergeInstances()`](advanced-creation.md#merging-instances).
-
### Experimental HTTP2 support
Got provides an experimental support for HTTP2 using the [`http2-wrapper`](https://github.com/szmarczak/http2-wrapper) package:
diff --git a/source/as-promise.ts b/source/as-promise.ts
index b65628079..08448c60d 100644
--- a/source/as-promise.ts
+++ b/source/as-promise.ts
@@ -9,7 +9,9 @@ import {ParseError, ReadError, HTTPError} from './errors';
import {reNormalizeArguments} from './normalize-arguments';
import requestAsEventEmitter from './request-as-event-emitter';
-type ResponeReturn = Response | Buffer | string | any;
+type ResponseReturn = Response | Buffer | string | any;
+
+export const isProxiedSymbol = Symbol('proxied');
export default function asPromise(options: NormalizedOptions): CancelableRequest {
const proxy = new EventEmitter();
@@ -130,7 +132,9 @@ export default function asPromise(options: NormalizedOptions): CancelableRequest
].forEach(event => emitter.on(event, (...args: unknown[]) => {
proxy.emit(event, ...args);
}));
- }) as CancelableRequest;
+ }) as CancelableRequest;
+
+ promise[isProxiedSymbol] = true;
promise.on = (name, fn) => {
proxy.on(name, fn);
diff --git a/source/create.ts b/source/create.ts
index a82aac530..01823ed37 100644
--- a/source/create.ts
+++ b/source/create.ts
@@ -6,21 +6,24 @@ import {
Response,
CancelableRequest,
URLOrOptions,
- URLArgument
+ URLArgument,
+ HandlerFunction,
+ ExtendedOptions
} from './utils/types';
import deepFreeze from './utils/deep-freeze';
-import merge, {mergeOptions, mergeInstances} from './merge';
-import asPromise from './as-promise';
+import merge, {mergeOptions} from './merge';
+import asPromise, {isProxiedSymbol} from './as-promise';
import asStream, {ProxyStream} from './as-stream';
import {preNormalizeArguments, normalizeArguments} from './normalize-arguments';
import {Hooks} from './known-hook-events';
-const getPromiseOrStream = (options: NormalizedOptions): ProxyStream | CancelableRequest => options.stream ? asStream(options) : asPromise(options);
-
export type HTTPAlias = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete';
export type ReturnResponse = (url: URLArgument | Options & { stream?: false; url: URLArgument }, options?: Options & { stream?: false }) => CancelableRequest;
export type ReturnStream = (url: URLArgument | Options & { stream: true; url: URLArgument }, options?: Options & { stream: true }) => ProxyStream;
+export type GotReturn = ProxyStream | CancelableRequest;
+
+const getPromiseOrStream = (options: NormalizedOptions): GotReturn => options.stream ? asStream(options) : asPromise(options);
export interface Got extends Record {
stream: GotStream;
@@ -40,8 +43,8 @@ export interface Got extends Record {
(url: URLArgument | Options & { stream: true; url: URLArgument }, options?: Options & { stream: true }): ProxyStream;
(url: URLOrOptions, options?: Options): CancelableRequest | ProxyStream;
create(defaults: Defaults): Got;
- extend(options?: Options): Got;
- mergeInstances(...instances: Got[]): Got;
+ extend(...instancesOrOptions: Array): Got;
+ mergeInstances(parent: Got, ...instances: Got[]): Got;
mergeOptions(...sources: T[]): T & { hooks: Partial };
}
@@ -58,22 +61,65 @@ const aliases: readonly HTTPAlias[] = [
'delete'
];
+const defaultHandler: HandlerFunction = (options, next) => next(options);
+
+// `got.mergeInstances()` is deprecated
+let hasShownDeprecation = false;
+
const create = (defaults: Partial): Got => {
defaults = merge>({}, defaults);
preNormalizeArguments(defaults.options!);
- if (!defaults.handler) {
- // This can't be getPromiseOrStream, because when merging
- // the chain would stop at this point and no further handlers would be called.
- defaults.handler = (options, next) => next(options);
- }
+ defaults = {
+ handlers: [defaultHandler],
+ options: {},
+ ...defaults,
+ mutableDefaults: Boolean(defaults.mutableDefaults)
+ };
// @ts-ignore Because the for loop handles it for us, as well as the other Object.defines
- const got: Got = (url: URLOrOptions, options?: Options): ProxyStream | CancelableRequest => {
+ const got: Got = (url: URLOrOptions, options?: Options): GotReturn => {
+ const isStream = options && options.stream;
+
+ let iteration = 0;
+ const iterateHandlers = (newOptions: NormalizedOptions): GotReturn => {
+ let nextPromise: CancelableRequest;
+ 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;
+ 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).cancel = nextPromise.cancel;
+ result[isProxiedSymbol] = true;
+ }
+
+ return result;
+ };
+
try {
- return defaults.handler!(normalizeArguments(url, options as NormalizedOptions, defaults), getPromiseOrStream);
+ return iterateHandlers(normalizeArguments(url, options as NormalizedOptions, defaults));
} catch (error) {
- if (options && options.stream) {
+ if (isStream) {
throw error;
} else {
// @ts-ignore It's an Error not a response, but TS thinks it's calling .resolve
@@ -83,23 +129,45 @@ const create = (defaults: Partial): Got => {
};
got.create = create;
- got.extend = options => {
+ got.extend = (...instancesOrOptions) => {
+ const options: Options[] = [defaults.options];
+ const handlers: HandlerFunction[] = [...defaults.handlers];
let mutableDefaults: boolean;
- if (options && Reflect.has(options, 'mutableDefaults')) {
- mutableDefaults = options.mutableDefaults!;
- delete options.mutableDefaults;
- } else {
- mutableDefaults = defaults.mutableDefaults!;
+
+ 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));
+
+ mutableDefaults = (value as Got).defaults.mutableDefaults;
+ } else {
+ options.push(value as Options);
+
+ if (Reflect.has(value, 'handlers')) {
+ handlers.push(...(value as ExtendedOptions).handlers);
+ }
+
+ mutableDefaults = (value as ExtendedOptions).mutableDefaults;
+ }
}
+ handlers.push(defaultHandler);
+
return create({
- options: mergeOptions(defaults.options!, options!),
- handler: defaults.handler,
+ options: mergeOptions(...options),
+ handlers,
mutableDefaults
});
};
- got.mergeInstances = (...args: Got[]) => create(mergeInstances(args));
+ 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});
diff --git a/source/merge.ts b/source/merge.ts
index a12f24190..baa1186e3 100644
--- a/source/merge.ts
+++ b/source/merge.ts
@@ -1,8 +1,6 @@
import is from '@sindresorhus/is';
-import {Options, Method, Defaults, NormalizedOptions, CancelableRequest, Response} from './utils/types';
+import {Options} from './utils/types';
import knownHookEvents, {Hooks, HookEvent, HookType} from './known-hook-events';
-import {Got} from './create';
-import {ProxyStream} from './as-stream';
const URLGlobal: typeof URL = typeof URL === 'undefined' ? require('url').URL : URL;
const URLSearchParamsGlobal: typeof URLSearchParams = typeof URLSearchParams === 'undefined' ? require('url').URLSearchParams : URLSearchParams;
@@ -86,19 +84,3 @@ export function mergeOptions(...sources: T[]): T & {hooks: Pa
return mergedOptions;
}
-
-export function mergeInstances(instances: Got[], methods?: Method[]): Defaults {
- const handlers = instances.map(instance => instance.defaults.handler);
- const size = instances.length - 1;
-
- return {
- methods,
- options: mergeOptions(...instances.map(instance => instance.defaults.options || {})),
- handler: >(options: NormalizedOptions, next: (options: NormalizedOptions) => T) => {
- let iteration = 0;
- const iterate = (newOptions: NormalizedOptions): T => handlers[++iteration]!(newOptions, iteration === size ? next : iterate);
-
- return iterate(options);
- }
- };
-}
diff --git a/source/utils/types.ts b/source/utils/types.ts
index b07b55f05..a46a8b6ec 100644
--- a/source/utils/types.ts
+++ b/source/utils/types.ts
@@ -69,7 +69,7 @@ export interface Response extends http.IncomingMessage {
export type RetryFunction = (retry: number, error: Error | GotError | ParseError | HTTPError | MaxRedirectsError) => number;
-export type HandlerFunction = >(options: Options, next: (options: Options) => T) => T;
+export type HandlerFunction = >(options: NormalizedOptions, next: (options: NormalizedOptions) => T) => T;
export interface RetryOption {
retries?: RetryFunction | number;
@@ -130,7 +130,6 @@ export interface Options extends Omit, 'timeout' | '
host: string;
}
+export interface ExtendedOptions extends Options {
+ handlers?: HandlerFunction[];
+ mutableDefaults?: boolean;
+}
+
export interface Defaults {
- methods?: Method[];
options?: Options;
- handler?: HandlerFunction;
+ handlers?: HandlerFunction[];
mutableDefaults?: boolean;
}
diff --git a/test/arguments.ts b/test/arguments.ts
index 1db6325c5..6f12ed30d 100644
--- a/test/arguments.ts
+++ b/test/arguments.ts
@@ -54,17 +54,18 @@ test('methods are normalized', withServer, async (t, server, got) => {
server.post('/test', echoUrl);
const instance = got.create({
- methods: got.defaults.methods,
options: got.defaults.options,
- handler: (options, next) => {
- if (options.method === options.method.toUpperCase()) {
- t.pass();
- } else {
- t.fail();
- }
+ handlers: [
+ (options, next) => {
+ if (options.method === options.method.toUpperCase()) {
+ t.pass();
+ } else {
+ t.fail();
+ }
- return next(options);
- }
+ return next(options);
+ }
+ ]
});
await instance('test', {method: 'post'});
@@ -232,12 +233,14 @@ test('backslash in the end of `prefixUrl` option is optional', withServer, async
test('throws when trying to modify `prefixUrl` after options got normalized', async t => {
const instanceA = got.create({
- methods: [],
options: {prefixUrl: 'https://example.com'},
- handler: (options, next) => {
- options.prefixUrl = 'https://google.com';
- return next(options);
- }
+ handlers: [
+ (options, next) => {
+ // @ts-ignore Even though we know it's read only, we need to test it.
+ options.prefixUrl = 'https://google.com';
+ return next(options);
+ }
+ ]
});
await t.throwsAsync(instanceA(''), 'Failed to set prefixUrl. Options are normalized already.');
diff --git a/test/create.ts b/test/create.ts
index 8804985f7..85db3e240 100644
--- a/test/create.ts
+++ b/test/create.ts
@@ -1,6 +1,7 @@
import http = require('http');
import {URL} from 'url';
import test from 'ava';
+import is from '@sindresorhus/is';
import got from '../source';
import withServer from './helpers/with-server';
@@ -108,10 +109,12 @@ test('create', withServer, async (t, server) => {
const instance = got.create({
options: {},
- handler: (options, next) => {
- options.headers.unicorn = 'rainbow';
- return next(options);
- }
+ handlers: [
+ (options, next) => {
+ options.headers.unicorn = 'rainbow';
+ return next(options);
+ }
+ ]
});
const headers = await instance(server.url).json();
t.is(headers.unicorn, 'rainbow');
@@ -139,14 +142,14 @@ test('custom endpoint with custom headers (extend)', withServer, async (t, serve
test('no tampering with defaults', t => {
const instance = got.create({
- handler: got.defaults.handler,
+ handlers: got.defaults.handlers,
options: got.mergeOptions(got.defaults.options, {
prefixUrl: 'example/'
})
});
const instance2 = instance.create({
- handler: instance.defaults.handler,
+ handlers: instance.defaults.handlers,
options: instance.defaults.options
});
@@ -246,3 +249,50 @@ test('hooks aren\'t overriden when merging options', withServer, async (t, serve
t.true(called);
});
+
+test('extend with custom handlers', withServer, async (t, server, got) => {
+ server.get('/', echoHeaders);
+
+ const instance = got.extend({
+ handlers: [
+ (options, next) => {
+ options.headers.unicorn = 'rainbow';
+ return next(options);
+ }
+ ]
+ });
+ const headers = await instance('').json();
+ t.is(headers.unicorn, 'rainbow');
+});
+
+test('extend with instances', t => {
+ const a = got.extend({prefixUrl: new URL('https://example.com/')});
+ const b = got.extend(a);
+ t.is(b.defaults.options.prefixUrl.toString(), 'https://example.com/');
+});
+
+test('extend with a chain', t => {
+ const a = got.extend({prefixUrl: 'https://example.com/'});
+ const b = got.extend(a, {headers: {foo: 'bar'}});
+ t.is(b.defaults.options.prefixUrl.toString(), 'https://example.com/');
+ t.is(b.defaults.options.headers.foo, 'bar');
+});
+
+test('async handlers', withServer, async (t, server, got) => {
+ server.get('/', echoHeaders);
+
+ const instance = got.extend({
+ handlers: [
+ async (options, next) => {
+ const result = await next(options);
+ result.modified = true;
+
+ return result;
+ }
+ ]
+ });
+
+ const promise = instance('');
+ t.true(is.function_(promise.cancel));
+ t.true((await promise).modified);
+});
diff --git a/test/merge-instances.ts b/test/merge-instances.ts
index f8bc02940..16b11c33f 100644
--- a/test/merge-instances.ts
+++ b/test/merge-instances.ts
@@ -14,7 +14,7 @@ test('merging instances', withServer, async (t, server) => {
const instanceA = got.extend({headers: {unicorn: 'rainbow'}});
const instanceB = got.extend({prefixUrl: server.url});
- const merged = got.mergeInstances(instanceA, instanceB);
+ const merged = instanceA.extend(instanceB);
const headers = await merged('').json();
t.is(headers.unicorn, 'rainbow');
@@ -25,16 +25,14 @@ test('works even if no default handler in the end', withServer, async (t, server
server.get('/', echoHeaders);
const instanceA = got.create({
- options: {},
- handler: (options, next) => next(options)
+ options: {}
});
const instanceB = got.create({
- options: {},
- handler: (options, next) => next(options)
+ options: {}
});
- const merged = got.mergeInstances(instanceA, instanceB);
+ const merged = instanceA.extend(instanceB);
await t.notThrowsAsync(() => merged(server.url));
});
@@ -44,12 +42,14 @@ test('merges default handlers & custom handlers', withServer, async (t, server)
const instanceA = got.extend({headers: {unicorn: 'rainbow'}});
const instanceB = got.create({
options: {},
- handler: (options, next) => {
- options.headers.cat = 'meow';
- return next(options);
- }
+ handlers: [
+ (options, next) => {
+ options.headers.cat = 'meow';
+ return next(options);
+ }
+ ]
});
- const merged = got.mergeInstances(instanceA, instanceB);
+ const merged = instanceA.extend(instanceB);
const headers = await merged(server.url).json();
t.is(headers.unicorn, 'rainbow');
@@ -64,8 +64,8 @@ test('merging one group & one instance', withServer, async (t, server) => {
const instanceC = got.extend({headers: {bird: 'tweet'}});
const instanceD = got.extend({headers: {mouse: 'squeek'}});
- const merged = got.mergeInstances(instanceA, instanceB, instanceC);
- const doubleMerged = got.mergeInstances(merged, instanceD);
+ const merged = instanceA.extend(instanceB, instanceC);
+ const doubleMerged = merged.extend(instanceD);
const headers = await doubleMerged(server.url).json();
t.is(headers.dog, 'woof');
@@ -82,10 +82,10 @@ test('merging two groups of merged instances', withServer, async (t, server) =>
const instanceC = got.extend({headers: {bird: 'tweet'}});
const instanceD = got.extend({headers: {mouse: 'squeek'}});
- const groupA = got.mergeInstances(instanceA, instanceB);
- const groupB = got.mergeInstances(instanceC, instanceD);
+ const groupA = instanceA.extend(instanceB);
+ const groupB = instanceC.extend(instanceD);
- const merged = got.mergeInstances(groupA, groupB);
+ const merged = groupA.extend(groupB);
const headers = await merged(server.url).json();
t.is(headers.dog, 'woof');
@@ -112,7 +112,7 @@ test('hooks are merged', t => {
]
}});
- const merged = got.mergeInstances(instanceA, instanceB);
+ const merged = instanceA.extend(instanceB);
t.deepEqual(getBeforeRequestHooks(merged), getBeforeRequestHooks(instanceA).concat(getBeforeRequestHooks(instanceB)));
});
@@ -131,7 +131,7 @@ test('hooks are passed by though other instances don\'t have them', t => {
options: {hooks: {}}
});
- const merged = got.mergeInstances(instanceA, instanceB, instanceC);
+ const merged = instanceA.extend(instanceB, instanceC);
t.deepEqual(merged.defaults.options.hooks.beforeRequest, instanceA.defaults.options.hooks.beforeRequest);
});
@@ -144,9 +144,22 @@ test('URLSearchParams instances are merged', t => {
searchParams: new URLSearchParams({b: '2'})
});
- const merged = got.mergeInstances(instanceA, instanceB);
+ 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);
+});