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); +});