Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: sindresorhus/got
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: bd3315b6c61d20a68944831d8b3a05046d5554ad
Choose a base ref
...
head repository: sindresorhus/got
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 5e17bb748c260b02e4cf716c2f4079a1c6a7481e
Choose a head ref

Commits on May 1, 2018

  1. Require Node.js 8

    sindresorhus committed May 1, 2018

    Unverified

    This user has not yet uploaded their public signing key.
    Copy the full SHA
    2b14537 View commit details
  2. Copy the full SHA
    00fdeea View commit details
  3. Copy the full SHA
    6e7a455 View commit details
  4. Copy the full SHA
    deabbec View commit details
  5. Copy the full SHA
    a03d21e View commit details

Commits on Jun 13, 2018

  1. Copy the full SHA
    54cead2 View commit details

Commits on Jul 3, 2018

  1. fix Buffer.byteLength(req._header) throwing error (#490)

    Anton Egorov authored and lukechilds committed Jul 3, 2018
    Copy the full SHA
    d7641e5 View commit details

Commits on Jul 5, 2018

  1. Copy the full SHA
    058452b View commit details
  2. Catch more errors (#498)

    szmarczak authored and sindresorhus committed Jul 5, 2018
    Copy the full SHA
    f621184 View commit details
  3. Copy the full SHA
    e473a26 View commit details
  4. Copy the full SHA
    74bbee3 View commit details
  5. Bump dependencies

    sindresorhus committed Jul 5, 2018
    Copy the full SHA
    b4f6698 View commit details
  6. Copy the full SHA
    2ee8124 View commit details
  7. Copy the full SHA
    013e668 View commit details
  8. Drop support for body being an Array when form: true

    `new URLSearchParams()` doesn't support this and it's a weird use-case. If you need support for this, just don't set `form: true` and handle stringifying yourself.
    sindresorhus committed Jul 5, 2018
    Copy the full SHA
    dfe5b1c View commit details
  9. Copy the full SHA
    8103bc5 View commit details
  10. Copy the full SHA
    38931e2 View commit details
  11. Code style tweaks

    sindresorhus committed Jul 5, 2018
    Copy the full SHA
    09eee39 View commit details
  12. Copy the full SHA
    b2dab3b View commit details
  13. Copy the full SHA
    346bafc View commit details
  14. Hide the electron import from Webpack

    Becuse Webpack is super annoying: webpack/webpack#196
    sindresorhus committed Jul 5, 2018
    Copy the full SHA
    a4ce0a7 View commit details
  15. Fix merging defaults and options (#499)

    Fixes commit 058452b
    szmarczak authored and sindresorhus committed Jul 5, 2018
    Copy the full SHA
    844c993 View commit details
  16. Document setting multiple cookies

    Fixes #408
    sindresorhus committed Jul 5, 2018
    Copy the full SHA
    0428961 View commit details
  17. Fix the proxy example in the readme

    Fixes #435
    sindresorhus committed Jul 5, 2018
    Copy the full SHA
    77bc901 View commit details

Commits on Jul 6, 2018

  1. Copy the full SHA
    b54b680 View commit details
  2. Meta tweaks

    sindresorhus committed Jul 6, 2018
    Copy the full SHA
    ab5e8e1 View commit details
  3. Copy the full SHA
    d485d7e View commit details

Commits on Jul 7, 2018

  1. Copy the full SHA
    464515f View commit details
  2. Fix timeouts (#501)

    Fixes #478
    Fixes #344
    szmarczak authored and sindresorhus committed Jul 7, 2018
    Copy the full SHA
    be0f17f View commit details
  3. Copy the full SHA
    57ff833 View commit details
  4. Enable AppVeyor

    sindresorhus committed Jul 7, 2018
    Copy the full SHA
    e599a8d View commit details
  5. Copy the full SHA
    ae01825 View commit details

Commits on Jul 8, 2018

  1. Update readme.md

    szmarczak authored Jul 8, 2018
    Copy the full SHA
    76f5847 View commit details
  2. Fix the timeout tests (#506)

    szmarczak authored and sindresorhus committed Jul 8, 2018
    Copy the full SHA
    75dc4c2 View commit details
  3. Simplify wrapping got (#503)

    This renames `got.create()` to `got.extend()` and adds a more powerful `got.create()` method.
    szmarczak authored and sindresorhus committed Jul 8, 2018
    Copy the full SHA
    bc41a49 View commit details

Commits on Jul 9, 2018

  1. Copy the full SHA
    e86aad7 View commit details
  2. Update readme.md

    szmarczak authored Jul 9, 2018
    Copy the full SHA
    112963e View commit details

Commits on Jul 10, 2018

  1. Copy the full SHA
    3eac42a View commit details

Commits on Jul 11, 2018

  1. Copy the full SHA
    28888fa View commit details
  2. Add tests for url-to-options.js (#513)

    alextes authored and sindresorhus committed Jul 11, 2018
    Copy the full SHA
    613fa9b View commit details
  3. Various minor code tweaks (#517)

    jstewmon authored and sindresorhus committed Jul 11, 2018
    Copy the full SHA
    7345a6e View commit details

Commits on Jul 12, 2018

  1. Change reqDelay to 160

    szmarczak authored Jul 12, 2018
    Copy the full SHA
    8fa2bf7 View commit details
  2. Copy the full SHA
    99e3835 View commit details
  3. Support retrying on some HTTP status codes and generally improve the …

    …retry functionality (#508)
    
    Fixes #417 
    Fixes #379
    szmarczak authored and sindresorhus committed Jul 12, 2018
    Copy the full SHA
    98b5664 View commit details

Commits on Jul 13, 2018

  1. Standardize on camelcase (#520)

    jstewmon authored and sindresorhus committed Jul 13, 2018
    Copy the full SHA
    fb5185a View commit details

Commits on Jul 14, 2018

  1. Add beforeRequest hook (#516)

    jstewmon authored and sindresorhus committed Jul 14, 2018
    Copy the full SHA
    107756f View commit details
  2. Copy the full SHA
    7a49ce7 View commit details

Commits on Jul 17, 2018

  1. Set headers on stream proxy (#518)

    Closes #401
    szmarczak authored and sindresorhus committed Jul 17, 2018
    Copy the full SHA
    83bc44c View commit details

Commits on Jul 18, 2018

  1. Properly clear requestTimeoutTimer timeout

    This file needs more tests.
    szmarczak authored Jul 18, 2018
    Copy the full SHA
    46d1217 View commit details
  2. Copy the full SHA
    13bb0fa View commit details
Showing with 17,527 additions and 3,121 deletions.
  1. +1 −2 .gitattributes
  2. +38 −0 .github/ISSUE_TEMPLATE/1-bug-report.md
  3. +18 −0 .github/ISSUE_TEMPLATE/2-feature-request.md
  4. +12 −0 .github/ISSUE_TEMPLATE/3-question.md
  5. +6 −0 .github/PULL_REQUEST_TEMPLATE.md
  6. +2 −0 .github/funding.yml
  7. +3 −0 .github/security.md
  8. +1 −0 .gitignore
  9. +39 −6 .travis.yml
  10. +196 −0 benchmark/index.ts
  11. +16 −0 benchmark/server.ts
  12. +126 −0 documentation/advanced-creation.md
  13. +61 −0 documentation/examples/gh-got.js
  14. +10 −0 documentation/examples/runkit-example.js
  15. +266 −0 documentation/lets-make-a-plugin.md
  16. +153 −0 documentation/migration-guides.md
  17. +0 −92 errors.js
  18. +0 −675 index.js
  19. BIN media/logo.sketch
  20. +1 −1 media/logo.svg
  21. +97 −62 package.json
  22. +2,088 −261 readme.md
  23. +31 −0 source/as-promise/create-rejection.ts
  24. +208 −0 source/as-promise/index.ts
  25. +98 −0 source/as-promise/normalize-arguments.ts
  26. +33 −0 source/as-promise/parse-body.ts
  27. +297 −0 source/as-promise/types.ts
  28. +37 −0 source/core/calculate-retry-delay.ts
  29. +2,861 −0 source/core/index.ts
  30. +20 −0 source/core/utils/dns-ip-version.ts
  31. +41 −0 source/core/utils/get-body-size.ts
  32. +21 −0 source/core/utils/get-buffer.ts
  33. +9 −0 source/core/utils/is-form-data.ts
  34. +8 −0 source/core/utils/is-response-ok.ts
  35. +73 −0 source/core/utils/options-to-url.ts
  36. +22 −0 source/core/utils/proxy-events.ts
  37. +178 −0 source/core/utils/timed-out.ts
  38. +40 −0 source/core/utils/unhandle.ts
  39. +43 −0 source/core/utils/url-to-options.ts
  40. +33 −0 source/core/utils/weakable-map.ts
  41. +322 −0 source/create.ts
  42. +133 −0 source/index.ts
  43. +391 −0 source/types.ts
  44. +11 −0 source/utils/deep-freeze.ts
  45. +14 −0 source/utils/deprecation-warning.ts
  46. +0 −154 test/agent.js
  47. +207 −0 test/agent.ts
  48. +0 −101 test/arguments.js
  49. +550 −0 test/arguments.ts
  50. +0 −107 test/cache.js
  51. +366 −0 test/cache.ts
  52. +0 −149 test/cancel.js
  53. +290 −0 test/cancel.ts
  54. +213 −0 test/cookies.ts
  55. +344 −0 test/create.ts
  56. +0 −95 test/error.js
  57. +326 −0 test/error.ts
  58. +1 −0 test/fixtures/ok
  59. +1 −0 test/fixtures/stream-content-length
  60. +0 −106 test/gzip.js
  61. +139 −0 test/gzip.ts
  62. +0 −138 test/headers.js
  63. +261 −0 test/headers.ts
  64. +0 −34 test/helpers.js
  65. +21 −0 test/helpers.ts
  66. +43 −0 test/helpers/create-http-test-server.ts
  67. +71 −0 test/helpers/create-https-test-server.ts
  68. +0 −40 test/helpers/server.js
  69. +17 −0 test/helpers/slow-data-stream.ts
  70. +15 −0 test/helpers/types.ts
  71. +122 −0 test/helpers/with-server.ts
  72. +1,256 −0 test/hooks.ts
  73. +0 −93 test/http.js
  74. +375 −0 test/http.ts
  75. +0 −67 test/https.js
  76. +468 −0 test/https.ts
  77. +0 −82 test/json-parse.js
  78. +177 −0 test/merge-instances.ts
  79. +118 −0 test/normalize-arguments.ts
  80. +690 −0 test/pagination.ts
  81. +0 −150 test/post.js
  82. +360 −0 test/post.ts
  83. +0 −190 test/progress.js
  84. +214 −0 test/progress.ts
  85. +97 −0 test/promise.ts
  86. +0 −212 test/redirects.js
  87. +540 −0 test/redirects.ts
  88. +257 −0 test/response-parse.ts
  89. +0 −76 test/retry.js
  90. +537 −0 test/retry.ts
  91. +0 −9 test/socket-destroyed.js
  92. +0 −116 test/stream.js
  93. +459 −0 test/stream.ts
  94. +0 −59 test/timeout.js
  95. +663 −0 test/timeout.ts
  96. +19 −0 test/types/create-test-server/index.d.ts
  97. +1 −0 test/types/slow-stream/index.d.ts
  98. +0 −44 test/unix-socket.js
  99. +70 −0 test/unix-socket.ts
  100. +143 −0 test/url-to-options.ts
  101. +22 −0 test/weakable-map.ts
  102. +16 −0 tsconfig.json
3 changes: 1 addition & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
* text=auto
* text=auto eol=lf
*.ai binary
*.js text eol=lf
38 changes: 38 additions & 0 deletions .github/ISSUE_TEMPLATE/1-bug-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
name: "🐞 Bug report"
about: Something is not working as it should
---

#### Describe the bug

- Node.js version:
- OS & version:

<!-- A clear and concise description of what the bug is. -->

#### Actual behavior

...

#### Expected behavior

...

#### Code to reproduce

```js
...
```

<!--
We encourage you to submit a pull request with a failing test:
- This will make it more likely for us to prioritize your issue.
- It's a good way to prove that the issue is related to Got and not your code.
Example: https://github.com/avajs/ava/blob/master/docs/01-writing-tests.md#failing-tests
-->

#### Checklist

- [ ] I have read the documentation.
- [ ] I have tried my code with the latest version of Node.js and Got.
18 changes: 18 additions & 0 deletions .github/ISSUE_TEMPLATE/2-feature-request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
name: "⭐ Feature request"
about: Suggest an idea for Got
---

#### What problem are you trying to solve?

...

#### Describe the feature

...

<!-- Include a usage example of the feature. If the feature is currently possible with a workaround, include that too. -->

#### Checklist

- [ ] I have read the documentation and made sure this feature doesn't already exist.
12 changes: 12 additions & 0 deletions .github/ISSUE_TEMPLATE/3-question.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: "❓ Question"
about: Something is unclear or needs to be discussed
---

#### What would you like to discuss?

...

#### Checklist

- [ ] I have read the documentation.
6 changes: 6 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#### Checklist

- [ ] I have read the documentation.
- [ ] I have included a pull request description of my changes.
- [ ] I have included some tests.
- [ ] If it's a new feature, I have included documentation updates in both the README and the types.
2 changes: 2 additions & 0 deletions .github/funding.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github: [sindresorhus, szmarczak]
tidelift: npm/got
3 changes: 3 additions & 0 deletions .github/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Security Policy

To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -2,3 +2,4 @@ node_modules
yarn.lock
coverage
.nyc_output
dist
45 changes: 39 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,40 @@
sudo: false
language: node_js
node_js:
- '8'
- '6'
- '4'
after_success: npm run coveralls

after_success:
- './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls'

jobs:
include:
- os: linux
dist: focal
node_js: '14'
- os: linux
dist: focal
node_js: '12'
- os: linux
dist: focal
node_js: '10'
- os: linux
dist: bionic
node_js: '14'
- os: linux
dist: bionic
node_js: '12'
- os: linux
dist: bionic
node_js: '10'
- os: windows
node_js: '14'
- os: windows
node_js: '12'
- os: windows
node_js: '10'
- os: osx
osx_image: xcode12
node_js: '14'
- os: osx
osx_image: xcode12
node_js: '12'
- os: osx
osx_image: xcode12
node_js: '10'
196 changes: 196 additions & 0 deletions benchmark/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
'use strict';
import {URL} from 'url';
import https = require('https');
import axios from 'axios';
import Benchmark = require('benchmark');
import fetch from 'node-fetch';
import request = require('request');
import got from '../source';
import Request, {kIsNormalizedAlready} from '../source/core';

const {normalizeArguments} = Request;

// Configuration
const httpsAgent = new https.Agent({
keepAlive: true,
rejectUnauthorized: false
});

const url = new URL('https://127.0.0.1:8080');
const urlString = url.toString();

const gotOptions = {
agent: {
https: httpsAgent
},
https: {
rejectUnauthorized: false
},
retry: 0
};

const normalizedGotOptions = normalizeArguments(url, gotOptions);
normalizedGotOptions[kIsNormalizedAlready] = true;

const requestOptions = {
strictSSL: false,
agent: httpsAgent
};

const fetchOptions = {
agent: httpsAgent
};

const axiosOptions = {
url: urlString,
httpsAgent,
https: {
rejectUnauthorized: false
}
};

const axiosStreamOptions: typeof axiosOptions & {responseType: 'stream'} = {
...axiosOptions,
responseType: 'stream'
};

const httpsOptions = {
https: {
rejectUnauthorized: false
},
agent: httpsAgent
};

const suite = new Benchmark.Suite();

// Benchmarking
suite.add('got - promise', {
defer: true,
fn: async (deferred: {resolve: () => void}) => {
await got(url, gotOptions);
deferred.resolve();
}
}).add('got - stream', {
defer: true,
fn: async (deferred: {resolve: () => void}) => {
got.stream(url, gotOptions).resume().once('end', () => {
deferred.resolve();
});
}
}).add('got - core', {
defer: true,
fn: async (deferred: {resolve: () => void}) => {
const stream = new Request(url, gotOptions);
stream.resume().once('end', () => {
deferred.resolve();
});
}
}).add('got - core - normalized options', {
defer: true,
fn: async (deferred: {resolve: () => void}) => {
const stream = new Request(undefined as any, normalizedGotOptions);
stream.resume().once('end', () => {
deferred.resolve();
});
}
}).add('request - callback', {
defer: true,
fn: (deferred: {resolve: () => void}) => {
request(urlString, requestOptions, (error: Error) => {
if (error) {
throw error;
}

deferred.resolve();
});
}
}).add('request - stream', {
defer: true,
fn: (deferred: {resolve: () => void}) => {
const stream = request(urlString, requestOptions);
stream.resume();
stream.once('end', () => {
deferred.resolve();
});
}
}).add('node-fetch - promise', {
defer: true,
fn: async (deferred: {resolve: () => void}) => {
const response = await fetch(url, fetchOptions);
await response.text();

deferred.resolve();
}
}).add('node-fetch - stream', {
defer: true,
fn: async (deferred: {resolve: () => void}) => {
const {body} = await fetch(url, fetchOptions);

body.resume();
body.once('end', () => {
deferred.resolve();
});
}
}).add('axios - promise', {
defer: true,
fn: async (deferred: {resolve: () => void}) => {
await axios.request(axiosOptions);
deferred.resolve();
}
}).add('axios - stream', {
defer: true,
fn: async (deferred: {resolve: () => void}) => {
const {data} = await axios.request(axiosStreamOptions);
data.resume();
data.once('end', () => {
deferred.resolve();
});
}
}).add('https - stream', {
defer: true,
fn: (deferred: {resolve: () => void}) => {
https.request(urlString, httpsOptions, response => {
response.resume();
response.once('end', () => {
deferred.resolve();
});
}).end();
}
}).on('cycle', (event: Benchmark.Event) => {
console.log(String(event.target));
}).on('complete', function (this: any) {
console.log(`Fastest is ${this.filter('fastest').map('name') as string}`);

internalBenchmark();
}).run();

const internalBenchmark = (): void => {
console.log();

const internalSuite = new Benchmark.Suite();
internalSuite.add('got - normalize options', {
fn: () => {
normalizeArguments(url, gotOptions);
}
}).on('cycle', (event: Benchmark.Event) => {
console.log(String(event.target));
});

internalSuite.run();
};

// Results (i7-7700k, CPU governor: performance):
// got - promise x 3,003 ops/sec ±6.26% (70 runs sampled)
// got - stream x 3,538 ops/sec ±5.86% (67 runs sampled)
// got - core x 5,828 ops/sec ±3.11% (79 runs sampled)
// got - core - normalized options x 7,596 ops/sec ±1.60% (85 runs sampled)
// request - callback x 6,530 ops/sec ±6.84% (72 runs sampled)
// request - stream x 7,348 ops/sec ±3.62% (78 runs sampled)
// node-fetch - promise x 6,284 ops/sec ±5.50% (76 runs sampled)
// node-fetch - stream x 7,746 ops/sec ±3.32% (80 runs sampled)
// axios - promise x 6,301 ops/sec ±6.24% (77 runs sampled)
// axios - stream x 8,605 ops/sec ±2.73% (87 runs sampled)
// https - stream x 10,477 ops/sec ±3.64% (80 runs sampled)
// Fastest is https - stream

// got - normalize options x 90,974 ops/sec ±0.57% (93 runs sampled)
16 changes: 16 additions & 0 deletions benchmark/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {AddressInfo} from 'net';
import https = require('https');
// @ts-expect-error No types
import createCert = require('create-cert');

(async () => {
const keys = await createCert({days: 365, commonName: 'localhost'});

const server = https.createServer(keys, (_request, response) => {
response.end('ok');
}).listen(8080, () => {
const {port} = server.address() as AddressInfo;

console.log(`Listening at https://localhost:${port}`);
});
})();
126 changes: 126 additions & 0 deletions documentation/advanced-creation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Advanced creation

> Make calling REST APIs easier by creating niche-specific `got` instances.
### 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': undefined
}
});
```

#### 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('https://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
})();
```
61 changes: 61 additions & 0 deletions documentation/examples/gh-got.js
Original file line number Diff line number Diff line change
@@ -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({
prefixUrl: '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.isStream) {
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} (${response.statusCode} status code)`;
}

// Rate limit for errors
if (response) {
error.rateLimit = getRateLimit(response.headers);
}

throw error;
}
})();
}
]
});

module.exports = instance;
10 changes: 10 additions & 0 deletions documentation/examples/runkit-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const got = require('got');

(async () => {
const issUrl = 'http://api.open-notify.org/iss-now.json';

const {iss_position: issPosition} = await got(issUrl).json();

console.log(issPosition);
//=> {latitude: '20.4956', longitude: '42.2216'}
})();
266 changes: 266 additions & 0 deletions documentation/lets-make-a-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
# 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 `prefixUrl` option:

```js
const got = require('got');

const instance = got.extend({
prefixUrl: '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({
prefixUrl: 'https://api.github.com',
headers: {
accept: 'application/vnd.github.v3+json'
}
});

module.exports = instance;
```

### JSON body

We'll use [`options.responseType`](../readme.md#responsetype):

```js
const got = require('got');

const instance = got.extend({
prefixUrl: '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({
prefixUrl: '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.isStream) {
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} (${response.statusCode} status code)`;
}

throw error;
}
})();
}
]
...
```

### Rate limiting

Umm... `response.headers['x-ratelimit-remaining']` doesn't look good. What about `response.rateLimit.limit` instead?<br>
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.isStream) {
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} (${response.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.
153 changes: 153 additions & 0 deletions documentation/migration-guides.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Migration guides

> :star: Switching from other HTTP request libraries to Got :star:
### Migrating from Request

You may think it's too hard to switch, but it's really not. 🦄

Let's take the very first example from Request's readme:

```js
const request = require('request');

request('https://google.com', (error, response, body) => {
console.log('error:', error);
console.log('statusCode:', response && response.statusCode);
console.log('body:', body);
});
```

With Got, it is:

```js
const got = require('got');

(async () => {
try {
const response = await got('https://google.com');
console.log('statusCode:', response.statusCode);
console.log('body:', response.body);
} catch (error) {
console.log('error:', error);
}
})();
```

Looks better now, huh? 😎

#### Common options

Both Request and Got accept [`http.request` options](https://nodejs.org/api/http.html#http_http_request_options_callback).

These Got options are the same as with Request:

- [`url`](https://github.com/sindresorhus/got#url) (+ we accept [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) instances too!)
- [`body`](https://github.com/sindresorhus/got#body)
- [`followRedirect`](https://github.com/sindresorhus/got#followRedirect)
- [`encoding`](https://github.com/sindresorhus/got#encoding)
- [`maxRedirects`](https://github.com/sindresorhus/got#maxredirects)

So if you're familiar with them, you're good to go.

Oh, and one more thing... There's no `time` option. Assume [it's always true](https://github.com/sindresorhus/got#timings).

#### Renamed options

Readability is very important to us, so we have different names for these options:

- `qs`[`searchParams`](https://github.com/sindresorhus/got#searchParams)
- `strictSSL`[`rejectUnauthorized`](https://github.com/sindresorhus/got#rejectUnauthorized)
- `gzip`[`decompress`](https://github.com/sindresorhus/got#decompress)
- `jar`[`cookieJar`](https://github.com/sindresorhus/got#cookiejar) (accepts [`tough-cookie`](https://github.com/salesforce/tough-cookie) jar)

It's more clear, isn't it?

#### Changes in behavior

The [`timeout` option](https://github.com/sindresorhus/got#timeout) has some extra features. You can [set timeouts on particular events](../readme.md#timeout)!

The [`searchParams` option](https://github.com/sindresorhus/got#searchParams) is always serialized using [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) unless it's a `string`.

To use streams, just call `got.stream(url, options)` or `got(url, {isStream: true, ...}`).

#### Breaking changes

- The `json` option is not a `boolean`, it's an `Object`. It will be stringified and used as a body.
- The `form` option is an `Object`. It can be a plain object or a [`form-data` instance](https://github.com/sindresorhus/got/#form-data).
- Got will lowercase all custom headers, even if they are specified to not be.
- No `oauth`/`hawk`/`aws`/`httpSignature` option. To sign requests, you need to create a [custom instance](advanced-creation.md#signing-requests).
- No `agentClass`/`agentOptions`/`pool` option.
- No `forever` option. You need to use [forever-agent](https://github.com/request/forever-agent).
- No `proxy` option. You need to [pass a custom agent](../readme.md#proxies).
- No `auth` option. You need to use `username` / `password` instead.
- No `baseUrl` option. Instead, there is `prefixUrl` which appends a trailing slash if not present. It will be always prepended unless `url` is an instance of URL.
- No `removeRefererHeader` option. You can remove the referer header in a [`beforeRequest` hook](https://github.com/sindresorhus/got#hooksbeforeRequest):

```js
const gotInstance = got.extend({
hooks: {
beforeRequest: [
options => {
delete options.headers.referer;
}
]
}
});

gotInstance(url, options);
```

- No `jsonReviver`/`jsonReplacer` option, but you can use `parseJson`/`stringifyJson` for that:

```js
const gotInstance = got.extend({
parseJson: text => JSON.parse(text, myJsonReviver),
stringifyJson: object => JSON.stringify(object, myJsonReplacer)
});

gotInstance(url, options);
```

Hooks are powerful, aren't they? [Read more](../readme.md#hooks) to see what else you achieve using hooks.

#### More about streams

Let's take a quick look at another example from Request's readme:

```js
http.createServer((serverRequest, serverResponse) => {
if (serverRequest.url === '/doodle.png') {
serverRequest.pipe(request('https://example.com/doodle.png')).pipe(serverResponse);
}
});
```

The cool feature here is that Request can proxy headers with the stream, but Got can do that too:

```js
const stream = require('stream');
const {promisify} = require('util');
const got = require('got');

const pipeline = promisify(stream.pipeline);

http.createServer(async (serverRequest, serverResponse) => {
if (serverRequest.url === '/doodle.png') {
// When someone makes a request to our server, we receive a body and some headers.
// These are passed to Got. Got proxies downloaded data to our server response,
// so you don't have to do `response.writeHead(statusCode, headers)` and `response.end(body)`.
// It's done automatically.
await pipeline(
got.stream('https://example.com/doodle.png'),
serverResponse
);
}
});
```

Nothing has really changed. Just remember to use `got.stream(url, options)` or `got(url, {isStream: true, …})`. That's it!

#### You're good to go!

Well, you have already come this far :tada: Take a look at the [documentation](../readme.md#highlights). It's worth the time to read it. There are [some great tips](../readme.md#aborting-the-request). If something is unclear or doesn't work as it should, don't hesitate to [open an issue](https://github.com/sindresorhus/got/issues/new/choose).
92 changes: 0 additions & 92 deletions errors.js

This file was deleted.

675 changes: 0 additions & 675 deletions index.js

This file was deleted.

Binary file added media/logo.sketch
Binary file not shown.
2 changes: 1 addition & 1 deletion media/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
159 changes: 97 additions & 62 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,95 +1,130 @@
{
"name": "got",
"version": "8.3.1",
"description": "Simplified HTTP requests",
"version": "11.8.5",
"description": "Human-friendly and powerful HTTP request library for Node.js",
"license": "MIT",
"repository": "sindresorhus/got",
"maintainers": [
{
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "sindresorhus.com"
},
{
"name": "Vsevolod Strukchinsky",
"email": "floatdrop@gmail.com",
"url": "github.com/floatdrop"
},
{
"name": "Alexander Tesfamichael",
"email": "alex.tesfamichael@gmail.com",
"url": "alextes.me"
}
],
"funding": "https://github.com/sindresorhus/got?sponsor=1",
"main": "dist/source",
"engines": {
"node": ">=4"
"node": ">=10.19.0"
},
"scripts": {
"test": "xo && nyc ava",
"coveralls": "nyc report --reporter=text-lcov | coveralls"
"test": "xo && npm run build && nyc --reporter=html --reporter=text ava",
"release": "np",
"build": "del-cli dist && tsc",
"prepare": "npm run build"
},
"files": [
"index.js",
"errors.js"
"dist/source"
],
"keywords": [
"http",
"https",
"http2",
"get",
"got",
"url",
"uri",
"request",
"util",
"utility",
"simple",
"curl",
"wget",
"fetch",
"net",
"network",
"electron"
"gzip",
"brotli",
"requests",
"human-friendly",
"axios",
"superagent",
"node-fetch",
"ky"
],
"dependencies": {
"@sindresorhus/is": "^0.7.0",
"cacheable-request": "^2.1.1",
"decompress-response": "^3.3.0",
"duplexer3": "^0.1.4",
"get-stream": "^3.0.0",
"into-stream": "^3.1.0",
"is-retry-allowed": "^1.1.0",
"isurl": "^1.0.0-alpha5",
"lowercase-keys": "^1.0.0",
"mimic-response": "^1.0.0",
"p-cancelable": "^0.4.0",
"p-timeout": "^2.0.1",
"pify": "^3.0.0",
"safe-buffer": "^5.1.1",
"timed-out": "^4.0.1",
"url-parse-lax": "^3.0.0",
"url-to-options": "^1.0.1"
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
"p-cancelable": "^2.0.0",
"responselike": "^2.0.0"
},
"devDependencies": {
"ava": "^0.25.0",
"coveralls": "^3.0.0",
"form-data": "^2.1.1",
"get-port": "^3.0.0",
"nyc": "^11.0.2",
"p-event": "^1.3.0",
"pem": "^1.4.4",
"proxyquire": "^1.8.0",
"sinon": "^4.0.0",
"@ava/typescript": "^1.1.1",
"@sindresorhus/tsconfig": "^0.7.0",
"@sinonjs/fake-timers": "^6.0.1",
"@types/benchmark": "^1.0.33",
"@types/express": "^4.17.7",
"@types/node": "^14.14.0",
"@types/node-fetch": "^2.5.7",
"@types/pem": "^1.9.5",
"@types/pify": "^3.0.2",
"@types/request": "^2.48.5",
"@types/sinon": "^9.0.5",
"@types/tough-cookie": "^4.0.0",
"ava": "^3.11.1",
"axios": "^0.20.0",
"benchmark": "^2.1.4",
"coveralls": "^3.1.0",
"create-test-server": "^3.0.1",
"del-cli": "^3.0.1",
"delay": "^4.4.0",
"express": "^4.17.1",
"form-data": "^3.0.0",
"get-stream": "^6.0.0",
"nock": "^13.0.4",
"node-fetch": "^2.6.0",
"np": "^6.4.0",
"nyc": "^15.1.0",
"p-event": "^4.2.0",
"pem": "^1.14.4",
"pify": "^5.0.0",
"sinon": "^9.0.3",
"slow-stream": "0.0.4",
"tempfile": "^2.0.0",
"tempy": "^0.2.1",
"universal-url": "1.0.0-alpha",
"xo": "^0.20.0"
"tempy": "^1.0.0",
"to-readable-stream": "^2.1.0",
"tough-cookie": "^4.0.0",
"typescript": "4.0.3",
"xo": "^0.34.1"
},
"types": "dist/source",
"sideEffects": false,
"ava": {
"concurrency": 4
"files": [
"test/*"
],
"timeout": "1m",
"typescript": {
"rewritePaths": {
"test/": "dist/test/"
}
}
},
"nyc": {
"extension": [
".ts"
],
"exclude": [
"**/test/**"
]
},
"xo": {
"ignores": [
"documentation/examples/*"
],
"rules": {
"@typescript-eslint/no-empty-function": "off",
"node/prefer-global/url": "off",
"node/prefer-global/url-search-params": "off",
"import/no-anonymous-default-export": "off",
"@typescript-eslint/no-implicit-any-catch": "off"
}
},
"browser": {
"decompress-response": false,
"electron": false
}
"runkitExampleFilename": "./documentation/examples/runkit-example.js"
}
2,349 changes: 2,088 additions & 261 deletions readme.md

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions source/as-promise/create-rejection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {CancelableRequest, BeforeErrorHook, RequestError} from './types';

export default function createRejection(error: Error, ...beforeErrorGroups: Array<BeforeErrorHook[] | undefined>): CancelableRequest<never> {
const promise = (async () => {
if (error instanceof RequestError) {
try {
for (const hooks of beforeErrorGroups) {
if (hooks) {
for (const hook of hooks) {
// eslint-disable-next-line no-await-in-loop
error = await hook(error as RequestError);
}
}
}
} catch (error_) {
error = error_;
}
}

throw error;
})() as CancelableRequest<never>;

const returnPromise = (): CancelableRequest<never> => promise;

promise.json = returnPromise;
promise.text = returnPromise;
promise.buffer = returnPromise;
promise.on = returnPromise;

return promise;
}
208 changes: 208 additions & 0 deletions source/as-promise/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import {EventEmitter} from 'events';
import is from '@sindresorhus/is';
import PCancelable = require('p-cancelable');
import {
NormalizedOptions,
CancelableRequest,
Response,
RequestError,
HTTPError,
CancelError
} from './types';
import parseBody from './parse-body';
import Request from '../core';
import proxyEvents from '../core/utils/proxy-events';
import getBuffer from '../core/utils/get-buffer';
import {isResponseOk} from '../core/utils/is-response-ok';

const proxiedRequestEvents = [
'request',
'response',
'redirect',
'uploadProgress',
'downloadProgress'
];

export default function asPromise<T>(normalizedOptions: NormalizedOptions): CancelableRequest<T> {
let globalRequest: Request;
let globalResponse: Response;
const emitter = new EventEmitter();

const promise = new PCancelable<T>((resolve, reject, onCancel) => {
const makeRequest = (retryCount: number): void => {
const request = new Request(undefined, normalizedOptions);
request.retryCount = retryCount;
request._noPipe = true;

onCancel(() => request.destroy());

onCancel.shouldReject = false;
onCancel(() => reject(new CancelError(request)));

globalRequest = request;

request.once('response', async (response: Response) => {
response.retryCount = retryCount;

if (response.request.aborted) {
// Canceled while downloading - will throw a `CancelError` or `TimeoutError` error
return;
}

// Download body
let rawBody;
try {
rawBody = await getBuffer(request);
response.rawBody = rawBody;
} catch {
// The same error is caught below.
// See request.once('error')
return;
}

if (request._isAboutToError) {
return;
}

// Parse body
const contentEncoding = (response.headers['content-encoding'] ?? '').toLowerCase();
const isCompressed = ['gzip', 'deflate', 'br'].includes(contentEncoding);

const {options} = request;

if (isCompressed && !options.decompress) {
response.body = rawBody;
} else {
try {
response.body = parseBody(response, options.responseType, options.parseJson, options.encoding);
} catch (error) {
// Fallback to `utf8`
response.body = rawBody.toString();

if (isResponseOk(response)) {
request._beforeError(error);
return;
}
}
}

try {
for (const [index, hook] of options.hooks.afterResponse.entries()) {
// @ts-expect-error TS doesn't notice that CancelableRequest is a Promise
// eslint-disable-next-line no-await-in-loop
response = await hook(response, async (updatedOptions): CancelableRequest<Response> => {
const typedOptions = Request.normalizeArguments(undefined, {
...updatedOptions,
retry: {
calculateDelay: () => 0
},
throwHttpErrors: false,
resolveBodyOnly: false
}, options);

// Remove any further hooks for that request, because we'll call them anyway.
// The loop continues. We don't want duplicates (asPromise recursion).
typedOptions.hooks.afterResponse = typedOptions.hooks.afterResponse.slice(0, index);

for (const hook of typedOptions.hooks.beforeRetry) {
// eslint-disable-next-line no-await-in-loop
await hook(typedOptions);
}

const promise: CancelableRequest<Response> = asPromise(typedOptions);

onCancel(() => {
promise.catch(() => {});
promise.cancel();
});

return promise;
});
}
} catch (error) {
request._beforeError(new RequestError(error.message, error, request));
return;
}

globalResponse = response;

if (!isResponseOk(response)) {
request._beforeError(new HTTPError(response));
return;
}

resolve(request.options.resolveBodyOnly ? response.body as T : response as unknown as T);
});

const onError = (error: RequestError) => {
if (promise.isCanceled) {
return;
}

const {options} = request;

if (error instanceof HTTPError && !options.throwHttpErrors) {
const {response} = error;
resolve(request.options.resolveBodyOnly ? response.body as T : response as unknown as T);
return;
}

reject(error);
};

request.once('error', onError);

const previousBody = request.options.body;

request.once('retry', (newRetryCount: number, error: RequestError) => {
if (previousBody === error.request?.options.body && is.nodeStream(error.request?.options.body)) {
onError(error);
return;
}

makeRequest(newRetryCount);
});

proxyEvents(request, emitter, proxiedRequestEvents);
};

makeRequest(0);
}) as CancelableRequest<T>;

promise.on = (event: string, fn: (...args: any[]) => void) => {
emitter.on(event, fn);
return promise;
};

const shortcut = <T>(responseType: NormalizedOptions['responseType']): CancelableRequest<T> => {
const newPromise = (async () => {
// Wait until downloading has ended
await promise;

const {options} = globalResponse.request;

return parseBody(globalResponse, responseType, options.parseJson, options.encoding);
})();

Object.defineProperties(newPromise, Object.getOwnPropertyDescriptors(promise));

return newPromise as CancelableRequest<T>;
};

promise.json = () => {
const {headers} = globalRequest.options;

if (!globalRequest.writableFinished && headers.accept === undefined) {
headers.accept = 'application/json';
}

return shortcut('json');
};

promise.buffer = () => shortcut('buffer');
promise.text = () => shortcut('text');

return promise;
}

export * from './types';
98 changes: 98 additions & 0 deletions source/as-promise/normalize-arguments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import is, {assert} from '@sindresorhus/is';
import {
Options,
NormalizedOptions,
Defaults,
Method
} from './types';

const normalizeArguments = (options: NormalizedOptions, defaults?: Defaults): NormalizedOptions => {
if (is.null_(options.encoding)) {
throw new TypeError('To get a Buffer, set `options.responseType` to `buffer` instead');
}

assert.any([is.string, is.undefined], options.encoding);
assert.any([is.boolean, is.undefined], options.resolveBodyOnly);
assert.any([is.boolean, is.undefined], options.methodRewriting);
assert.any([is.boolean, is.undefined], options.isStream);
assert.any([is.string, is.undefined], options.responseType);

// `options.responseType`
if (options.responseType === undefined) {
options.responseType = 'text';
}

// `options.retry`
const {retry} = options;

if (defaults) {
options.retry = {...defaults.retry};
} else {
options.retry = {
calculateDelay: retryObject => retryObject.computedValue,
limit: 0,
methods: [],
statusCodes: [],
errorCodes: [],
maxRetryAfter: undefined
};
}

if (is.object(retry)) {
options.retry = {
...options.retry,
...retry
};

options.retry.methods = [...new Set(options.retry.methods.map(method => method.toUpperCase() as Method))];
options.retry.statusCodes = [...new Set(options.retry.statusCodes)];
options.retry.errorCodes = [...new Set(options.retry.errorCodes)];
} else if (is.number(retry)) {
options.retry.limit = retry;
}

if (is.undefined(options.retry.maxRetryAfter)) {
options.retry.maxRetryAfter = Math.min(
// TypeScript is not smart enough to handle `.filter(x => is.number(x))`.
// eslint-disable-next-line unicorn/no-fn-reference-in-iterator
...[options.timeout.request, options.timeout.connect].filter(is.number)
);
}

// `options.pagination`
if (is.object(options.pagination)) {
if (defaults) {
(options as Options).pagination = {
...defaults.pagination,
...options.pagination
};
}

const {pagination} = options;

if (!is.function_(pagination.transform)) {
throw new Error('`options.pagination.transform` must be implemented');
}

if (!is.function_(pagination.shouldContinue)) {
throw new Error('`options.pagination.shouldContinue` must be implemented');
}

if (!is.function_(pagination.filter)) {
throw new TypeError('`options.pagination.filter` must be implemented');
}

if (!is.function_(pagination.paginate)) {
throw new Error('`options.pagination.paginate` must be implemented');
}
}

// JSON mode
if (options.responseType === 'json' && options.headers.accept === undefined) {
options.headers.accept = 'application/json';
}

return options;
};

export default normalizeArguments;
33 changes: 33 additions & 0 deletions source/as-promise/parse-body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
ResponseType,
ParseError,
Response,
ParseJsonFunction
} from './types';

const parseBody = (response: Response, responseType: ResponseType, parseJson: ParseJsonFunction, encoding?: BufferEncoding): unknown => {
const {rawBody} = response;

try {
if (responseType === 'text') {
return rawBody.toString(encoding);
}

if (responseType === 'json') {
return rawBody.length === 0 ? '' : parseJson(rawBody.toString());
}

if (responseType === 'buffer') {
return rawBody;
}

throw new ParseError({
message: `Unknown body type '${responseType as string}'`,
name: 'Error'
}, response);
} catch (error) {
throw new ParseError(error, response);
}
};

export default parseBody;
297 changes: 297 additions & 0 deletions source/as-promise/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
import PCancelable = require('p-cancelable');
import Request, {
Options,
Response,
RequestError,
RequestEvents
} from '../core';

/**
All parsing methods supported by Got.
*/
export type ResponseType = 'json' | 'buffer' | 'text';

export interface PaginationOptions<T, R> {
/**
All options accepted by `got.paginate()`.
*/
pagination?: {
/**
A function that transform [`Response`](#response) into an array of items.
This is where you should do the parsing.
@default response => JSON.parse(response.body)
*/
transform?: (response: Response<R>) => Promise<T[]> | T[];

/**
Checks whether the item should be emitted or not.
@default (item, allItems, currentItems) => true
*/
filter?: (item: T, allItems: T[], currentItems: T[]) => boolean;

/**
The function takes three arguments:
- `response` - The current response object.
- `allItems` - An array of the emitted items.
- `currentItems` - Items from the current response.
It should return an object representing Got options pointing to the next page.
The options are merged automatically with the previous request, therefore the options returned `pagination.paginate(...)` must reflect changes only.
If there are no more pages, `false` should be returned.
@example
```
const got = require('got');
(async () => {
const limit = 10;
const items = got.paginate('https://example.com/items', {
searchParams: {
limit,
offset: 0
},
pagination: {
paginate: (response, allItems, currentItems) => {
const previousSearchParams = response.request.options.searchParams;
const previousOffset = previousSearchParams.get('offset');
if (currentItems.length < limit) {
return false;
}
return {
searchParams: {
...previousSearchParams,
offset: Number(previousOffset) + limit,
}
};
}
}
});
console.log('Items from all pages:', items);
})();
```
*/
paginate?: (response: Response<R>, allItems: T[], currentItems: T[]) => Options | false;

/**
Checks whether the pagination should continue.
For example, if you need to stop **before** emitting an entry with some flag, you should use `(item, allItems, currentItems) => !item.flag`.
If you want to stop **after** emitting the entry, you should use `(item, allItems, currentItems) => allItems.some(entry => entry.flag)` instead.
@default (item, allItems, currentItems) => true
*/
shouldContinue?: (item: T, allItems: T[], currentItems: T[]) => boolean;

/**
The maximum amount of items that should be emitted.
@default Infinity
*/
countLimit?: number;

/**
Milliseconds to wait before the next request is triggered.
@default 0
*/
backoff?: number;
/**
The maximum amount of request that should be triggered.
Retries on failure are not counted towards this limit.
For example, it can be helpful during development to avoid an infinite number of requests.
@default 10000
*/
requestLimit?: number;

/**
Defines how the parameter `allItems` in pagination.paginate, pagination.filter and pagination.shouldContinue is managed.
When set to `false`, the parameter `allItems` is always an empty array.
This option can be helpful to save on memory usage when working with a large dataset.
*/
stackAllItems?: boolean;
};
}

export type AfterResponseHook = (response: Response, retryWithMergedOptions: (options: Options) => CancelableRequest<Response>) => Response | CancelableRequest<Response> | Promise<Response | CancelableRequest<Response>>;

// These should be merged into Options in core/index.ts
export namespace PromiseOnly {
export interface Hooks {
/**
Called with [response object](#response) and a retry function.
Calling the retry function will trigger `beforeRetry` hooks.
Each function should return the response.
This is especially useful when you want to refresh an access token.
__Note__: When using streams, this hook is ignored.
@example
```
const got = require('got');
const instance = got.extend({
hooks: {
afterResponse: [
(response, retryWithMergedOptions) => {
if (response.statusCode === 401) { // Unauthorized
const updatedOptions = {
headers: {
token: getNewToken() // Refresh the access token
}
};
// Save for further requests
instance.defaults.options = got.mergeOptions(instance.defaults.options, updatedOptions);
// Make a new retry
return retryWithMergedOptions(updatedOptions);
}
// No changes otherwise
return response;
}
],
beforeRetry: [
(options, error, retryCount) => {
// This will be called on `retryWithMergedOptions(...)`
}
]
},
mutableDefaults: true
});
```
*/
afterResponse?: AfterResponseHook[];
}

export interface Options extends PaginationOptions<unknown, unknown> {
/**
The parsing method.
The promise also has `.text()`, `.json()` and `.buffer()` methods which return another Got promise for the parsed body.
It's like setting the options to `{responseType: 'json', resolveBodyOnly: true}` but without affecting the main Got promise.
__Note__: When using streams, this option is ignored.
@example
```
(async () => {
const responsePromise = got(url);
const bufferPromise = responsePromise.buffer();
const jsonPromise = responsePromise.json();
const [response, buffer, json] = Promise.all([responsePromise, bufferPromise, jsonPromise]);
// `response` is an instance of Got Response
// `buffer` is an instance of Buffer
// `json` is an object
})();
```
@example
```
// This
const body = await got(url).json();
// is semantically the same as this
const body = await got(url, {responseType: 'json', resolveBodyOnly: true});
```
*/
responseType?: ResponseType;

/**
When set to `true` the promise will return the Response body instead of the Response object.
@default false
*/
resolveBodyOnly?: boolean;

/**
Returns a `Stream` instead of a `Promise`.
This is equivalent to calling `got.stream(url, options?)`.
@default false
*/
isStream?: boolean;

/**
[Encoding](https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings) to be used on `setEncoding` of the response data.
To get a [`Buffer`](https://nodejs.org/api/buffer.html), you need to set `responseType` to `buffer` instead.
Don't set this option to `null`.
__Note__: This doesn't affect streams! Instead, you need to do `got.stream(...).setEncoding(encoding)`.
@default 'utf-8'
*/
encoding?: BufferEncoding;
}

export interface NormalizedOptions {
responseType: ResponseType;
resolveBodyOnly: boolean;
isStream: boolean;
encoding?: BufferEncoding;
pagination?: Required<PaginationOptions<unknown, unknown>['pagination']>;
}

export interface Defaults {
responseType: ResponseType;
resolveBodyOnly: boolean;
isStream: boolean;
pagination?: Required<PaginationOptions<unknown, unknown>['pagination']>;
}

export type HookEvent = 'afterResponse';
}

/**
An error to be thrown when server response code is 2xx, and parsing body fails.
Includes a `response` property.
*/
export class ParseError extends RequestError {
declare readonly response: Response;

constructor(error: Error, response: Response) {
const {options} = response.request;

super(`${error.message} in "${options.url.toString()}"`, error, response.request);
this.name = 'ParseError';
this.code = this.code === 'ERR_GOT_REQUEST_ERROR' ? 'ERR_BODY_PARSE_FAILURE' : this.code;
}
}

/**
An error to be thrown when the request is aborted with `.cancel()`.
*/
export class CancelError extends RequestError {
declare readonly response: Response;

constructor(request: Request) {
super('Promise was canceled', {}, request);
this.name = 'CancelError';
this.code = 'ERR_CANCELED';
}

get isCanceled() {
return true;
}
}

export interface CancelableRequest<T extends Response | Response['body'] = Response['body']> extends PCancelable<T>, RequestEvents<CancelableRequest<T>> {
json: <ReturnType>() => CancelableRequest<ReturnType>;
buffer: () => CancelableRequest<Buffer>;
text: () => CancelableRequest<string>;
}

export * from '../core';
37 changes: 37 additions & 0 deletions source/core/calculate-retry-delay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {RetryFunction} from '.';

type Returns<T extends (...args: any) => unknown, V> = (...args: Parameters<T>) => V;

export const retryAfterStatusCodes: ReadonlySet<number> = new Set([413, 429, 503]);

const calculateRetryDelay: Returns<RetryFunction, number> = ({attemptCount, retryOptions, error, retryAfter}) => {
if (attemptCount > retryOptions.limit) {
return 0;
}

const hasMethod = retryOptions.methods.includes(error.options.method);
const hasErrorCode = retryOptions.errorCodes.includes(error.code);
const hasStatusCode = error.response && retryOptions.statusCodes.includes(error.response.statusCode);
if (!hasMethod || (!hasErrorCode && !hasStatusCode)) {
return 0;
}

if (error.response) {
if (retryAfter) {
if (retryOptions.maxRetryAfter === undefined || retryAfter > retryOptions.maxRetryAfter) {
return 0;
}

return retryAfter;
}

if (error.response.statusCode === 413) {
return 0;
}
}

const noise = Math.random() * 100;
return ((2 ** (attemptCount - 1)) * 1000) + noise;
};

export default calculateRetryDelay;
2,861 changes: 2,861 additions & 0 deletions source/core/index.ts

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions source/core/utils/dns-ip-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export type DnsLookupIpVersion = 'auto' | 'ipv4' | 'ipv6';
type DnsIpFamily = 0 | 4 | 6;

const conversionTable = {
auto: 0,
ipv4: 4,
ipv6: 6
};

export const isDnsLookupIpVersion = (value: any): boolean => {
return value in conversionTable;
};

export const dnsLookupIpVersionToFamily = (dnsLookupIpVersion: DnsLookupIpVersion): DnsIpFamily => {
if (isDnsLookupIpVersion(dnsLookupIpVersion)) {
return conversionTable[dnsLookupIpVersion] as DnsIpFamily;
}

throw new Error('Invalid DNS lookup IP version');
};
41 changes: 41 additions & 0 deletions source/core/utils/get-body-size.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {ReadStream, stat} from 'fs';
import {promisify} from 'util';
import {ClientRequestArgs} from 'http';
import is from '@sindresorhus/is';
import isFormData from './is-form-data';

const statAsync = promisify(stat);

export default async (body: unknown, headers: ClientRequestArgs['headers']): Promise<number | undefined> => {
if (headers && 'content-length' in headers) {
return Number(headers['content-length']);
}

if (!body) {
return 0;
}

if (is.string(body)) {
return Buffer.byteLength(body);
}

if (is.buffer(body)) {
return body.length;
}

if (isFormData(body)) {
return promisify(body.getLength.bind(body))();
}

if (body instanceof ReadStream) {
const {size} = await statAsync(body.path);

if (size === 0) {
return undefined;
}

return size;
}

return undefined;
};
21 changes: 21 additions & 0 deletions source/core/utils/get-buffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {Readable} from 'stream';

// TODO: Update https://github.com/sindresorhus/get-stream

const getBuffer = async (stream: Readable) => {
const chunks = [];
let length = 0;

for await (const chunk of stream) {
chunks.push(chunk);
length += Buffer.byteLength(chunk);
}

if (Buffer.isBuffer(chunks[0])) {
return Buffer.concat(chunks, length);
}

return Buffer.from(chunks.join(''));
};

export default getBuffer;
9 changes: 9 additions & 0 deletions source/core/utils/is-form-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import is from '@sindresorhus/is';
import {Readable} from 'stream';

interface FormData extends Readable {
getBoundary: () => string;
getLength: (callback: (error: Error | null, length: number) => void) => void;
}

export default (body: unknown): body is FormData => is.nodeStream(body) && is.function_((body as FormData).getBoundary);
8 changes: 8 additions & 0 deletions source/core/utils/is-response-ok.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {Response} from '..';

export const isResponseOk = (response: Response): boolean => {
const {statusCode} = response;
const limitStatusCode = response.request.options.followRedirect ? 299 : 399;

return (statusCode >= 200 && statusCode <= limitStatusCode) || statusCode === 304;
};
73 changes: 73 additions & 0 deletions source/core/utils/options-to-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* istanbul ignore file: deprecated */
import {URL} from 'url';

export interface URLOptions {
href?: string;
protocol?: string;
host?: string;
hostname?: string;
port?: string | number;
pathname?: string;
search?: string;
searchParams?: unknown;
path?: string;
}

const keys: Array<Exclude<keyof URLOptions, 'searchParams' | 'path'>> = [
'protocol',
'host',
'hostname',
'port',
'pathname',
'search'
];

export default (origin: string, options: URLOptions): URL => {
if (options.path) {
if (options.pathname) {
throw new TypeError('Parameters `path` and `pathname` are mutually exclusive.');
}

if (options.search) {
throw new TypeError('Parameters `path` and `search` are mutually exclusive.');
}

if (options.searchParams) {
throw new TypeError('Parameters `path` and `searchParams` are mutually exclusive.');
}
}

if (options.search && options.searchParams) {
throw new TypeError('Parameters `search` and `searchParams` are mutually exclusive.');
}

if (!origin) {
if (!options.protocol) {
throw new TypeError('No URL protocol specified');
}

origin = `${options.protocol}//${options.hostname ?? options.host ?? ''}`;
}

const url = new URL(origin);

if (options.path) {
const searchIndex = options.path.indexOf('?');
if (searchIndex === -1) {
options.pathname = options.path;
} else {
options.pathname = options.path.slice(0, searchIndex);
options.search = options.path.slice(searchIndex + 1);
}

delete options.path;
}

for (const key of keys) {
if (options[key]) {
url[key] = options[key]!.toString();
}
}

return url;
};
22 changes: 22 additions & 0 deletions source/core/utils/proxy-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {EventEmitter} from 'events';

type Fn = (...args: unknown[]) => void;
type Fns = Record<string, Fn>;

export default function (from: EventEmitter, to: EventEmitter, events: string[]): () => void {
const fns: Fns = {};

for (const event of events) {
fns[event] = (...args: unknown[]) => {
to.emit(event, ...args);
};

from.on(event, fns[event]);
}

return () => {
for (const event of events) {
from.off(event, fns[event]);
}
};
}
178 changes: 178 additions & 0 deletions source/core/utils/timed-out.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import net = require('net');
import {ClientRequest, IncomingMessage} from 'http';
import unhandler from './unhandle';

const reentry: unique symbol = Symbol('reentry');
const noop = (): void => {};

interface TimedOutOptions {
host?: string;
hostname?: string;
protocol?: string;
}

export interface Delays {
lookup?: number;
connect?: number;
secureConnect?: number;
socket?: number;
response?: number;
send?: number;
request?: number;
}

export type ErrorCode =
| 'ETIMEDOUT'
| 'ECONNRESET'
| 'EADDRINUSE'
| 'ECONNREFUSED'
| 'EPIPE'
| 'ENOTFOUND'
| 'ENETUNREACH'
| 'EAI_AGAIN';

export class TimeoutError extends Error {
code: ErrorCode;

constructor(threshold: number, public event: string) {
super(`Timeout awaiting '${event}' for ${threshold}ms`);

this.name = 'TimeoutError';
this.code = 'ETIMEDOUT';
}
}

export default (request: ClientRequest, delays: Delays, options: TimedOutOptions): () => void => {
if (reentry in request) {
return noop;
}

request[reentry] = true;
const cancelers: Array<typeof noop> = [];
const {once, unhandleAll} = unhandler();

const addTimeout = (delay: number, callback: (delay: number, event: string) => void, event: string): (typeof noop) => {
const timeout = setTimeout(callback, delay, delay, event) as unknown as NodeJS.Timeout;

timeout.unref?.();

const cancel = (): void => {
clearTimeout(timeout);
};

cancelers.push(cancel);

return cancel;
};

const {host, hostname} = options;

const timeoutHandler = (delay: number, event: string): void => {
request.destroy(new TimeoutError(delay, event));
};

const cancelTimeouts = (): void => {
for (const cancel of cancelers) {
cancel();
}

unhandleAll();
};

request.once('error', error => {
cancelTimeouts();

// Save original behavior
/* istanbul ignore next */
if (request.listenerCount('error') === 0) {
throw error;
}
});

request.once('close', cancelTimeouts);

once(request, 'response', (response: IncomingMessage): void => {
once(response, 'end', cancelTimeouts);
});

if (typeof delays.request !== 'undefined') {
addTimeout(delays.request, timeoutHandler, 'request');
}

if (typeof delays.socket !== 'undefined') {
const socketTimeoutHandler = (): void => {
timeoutHandler(delays.socket!, 'socket');
};

request.setTimeout(delays.socket, socketTimeoutHandler);

// `request.setTimeout(0)` causes a memory leak.
// We can just remove the listener and forget about the timer - it's unreffed.
// See https://github.com/sindresorhus/got/issues/690
cancelers.push(() => {
request.removeListener('timeout', socketTimeoutHandler);
});
}

once(request, 'socket', (socket: net.Socket): void => {
const {socketPath} = request as ClientRequest & {socketPath?: string};

/* istanbul ignore next: hard to test */
if (socket.connecting) {
const hasPath = Boolean(socketPath ?? net.isIP(hostname ?? host ?? '') !== 0);

if (typeof delays.lookup !== 'undefined' && !hasPath && typeof (socket.address() as net.AddressInfo).address === 'undefined') {
const cancelTimeout = addTimeout(delays.lookup, timeoutHandler, 'lookup');
once(socket, 'lookup', cancelTimeout);
}

if (typeof delays.connect !== 'undefined') {
const timeConnect = (): (() => void) => addTimeout(delays.connect!, timeoutHandler, 'connect');

if (hasPath) {
once(socket, 'connect', timeConnect());
} else {
once(socket, 'lookup', (error: Error): void => {
if (error === null) {
once(socket, 'connect', timeConnect());
}
});
}
}

if (typeof delays.secureConnect !== 'undefined' && options.protocol === 'https:') {
once(socket, 'connect', (): void => {
const cancelTimeout = addTimeout(delays.secureConnect!, timeoutHandler, 'secureConnect');
once(socket, 'secureConnect', cancelTimeout);
});
}
}

if (typeof delays.send !== 'undefined') {
const timeRequest = (): (() => void) => addTimeout(delays.send!, timeoutHandler, 'send');
/* istanbul ignore next: hard to test */
if (socket.connecting) {
once(socket, 'connect', (): void => {
once(request, 'upload-complete', timeRequest());
});
} else {
once(request, 'upload-complete', timeRequest());
}
}
});

if (typeof delays.response !== 'undefined') {
once(request, 'upload-complete', (): void => {
const cancelTimeout = addTimeout(delays.response!, timeoutHandler, 'response');
once(request, 'response', cancelTimeout);
});
}

return cancelTimeouts;
};

declare module 'http' {
interface ClientRequest {
[reentry]: boolean;
}
}
40 changes: 40 additions & 0 deletions source/core/utils/unhandle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {EventEmitter} from 'events';

type Origin = EventEmitter;
type Event = string | symbol;
type Fn = (...args: any[]) => void;

interface Handler {
origin: Origin;
event: Event;
fn: Fn;
}

interface Unhandler {
once: (origin: Origin, event: Event, fn: Fn) => void;
unhandleAll: () => void;
}

// When attaching listeners, it's very easy to forget about them.
// Especially if you do error handling and set timeouts.
// So instead of checking if it's proper to throw an error on every timeout ever,
// use this simple tool which will remove all listeners you have attached.
export default (): Unhandler => {
const handlers: Handler[] = [];

return {
once(origin: Origin, event: Event, fn: Fn) {
origin.once(event, fn);
handlers.push({origin, event, fn});
},

unhandleAll() {
for (const handler of handlers) {
const {origin, event, fn} = handler;
origin.removeListener(event, fn);
}

handlers.length = 0;
}
};
};
43 changes: 43 additions & 0 deletions source/core/utils/url-to-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {URL, UrlWithStringQuery} from 'url';
import is from '@sindresorhus/is';

// TODO: Deprecate legacy URL at some point

export interface LegacyUrlOptions {
protocol: string;
hostname: string;
host: string;
hash: string | null;
search: string | null;
pathname: string;
href: string;
path: string;
port?: number;
auth?: string;
}

export default (url: URL | UrlWithStringQuery): LegacyUrlOptions => {
// Cast to URL
url = url as URL;

const options: LegacyUrlOptions = {
protocol: url.protocol,
hostname: is.string(url.hostname) && url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname,
host: url.host,
hash: url.hash,
search: url.search,
pathname: url.pathname,
href: url.href,
path: `${url.pathname || ''}${url.search || ''}`
};

if (is.string(url.port) && url.port.length > 0) {
options.port = Number(url.port);
}

if (url.username || url.password) {
options.auth = `${url.username || ''}:${url.password || ''}`;
}

return options;
};
33 changes: 33 additions & 0 deletions source/core/utils/weakable-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export default class WeakableMap<K, V> {
weakMap: WeakMap<Record<string, unknown>, V>;
map: Map<K, V>;

constructor() {
this.weakMap = new WeakMap();
this.map = new Map();
}

set(key: K, value: V): void {
if (typeof key === 'object') {
this.weakMap.set(key as unknown as Record<string, unknown>, value);
} else {
this.map.set(key, value);
}
}

get(key: K): V | undefined {
if (typeof key === 'object') {
return this.weakMap.get(key as unknown as Record<string, unknown>);
}

return this.map.get(key);
}

has(key: K): boolean {
if (typeof key === 'object') {
return this.weakMap.has(key as unknown as Record<string, unknown>);
}

return this.map.has(key);
}
}
322 changes: 322 additions & 0 deletions source/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
import {URL} from 'url';
import is from '@sindresorhus/is';
import asPromise, {
// Response
Response,

// Options
Options,
NormalizedOptions,

// Hooks
InitHook,

// Errors
ParseError,
RequestError,
CacheError,
ReadError,
HTTPError,
MaxRedirectsError,
TimeoutError,
UnsupportedProtocolError,
UploadError,
CancelError
} from './as-promise';
import {
GotReturn,
ExtendOptions,
Got,
HTTPAlias,
HandlerFunction,
InstanceDefaults,
GotPaginate,
GotStream,
GotRequestFunction,
OptionsWithPagination,
StreamOptions
} from './types';
import createRejection from './as-promise/create-rejection';
import Request, {kIsNormalizedAlready, setNonEnumerableProperties, Defaults} from './core';
import deepFreeze from './utils/deep-freeze';

const errors = {
RequestError,
CacheError,
ReadError,
HTTPError,
MaxRedirectsError,
TimeoutError,
ParseError,
CancelError,
UnsupportedProtocolError,
UploadError
};

// The `delay` package weighs 10KB (!)
const delay = async (ms: number) => new Promise(resolve => {
setTimeout(resolve, ms);
});

const {normalizeArguments} = Request;

const mergeOptions = (...sources: Options[]): NormalizedOptions => {
let mergedOptions: NormalizedOptions | undefined;

for (const source of sources) {
mergedOptions = normalizeArguments(undefined, source, mergedOptions);
}

return mergedOptions!;
};

const getPromiseOrStream = (options: NormalizedOptions): GotReturn => options.isStream ? new Request(undefined, options) : asPromise(options);

const isGotInstance = (value: Got | ExtendOptions): value is Got => (
'defaults' in value && 'options' in value.defaults
);

const aliases: readonly HTTPAlias[] = [
'get',
'post',
'put',
'patch',
'head',
'delete'
];

export const defaultHandler: HandlerFunction = (options, next) => next(options);

const callInitHooks = (hooks: InitHook[] | undefined, options?: Options): void => {
if (hooks) {
for (const hook of hooks) {
hook(options!);
}
}
};

const create = (defaults: InstanceDefaults): Got => {
// Proxy properties from next handlers
defaults._rawHandlers = defaults.handlers;
defaults.handlers = defaults.handlers.map(fn => ((options, next) => {
// This will be assigned by assigning result
let root!: ReturnType<typeof next>;

const result = fn(options, newOptions => {
root = next(newOptions);
return root;
});

if (result !== root && !options.isStream && root) {
const typedResult = result as Promise<unknown>;

const {then: promiseThen, catch: promiseCatch, finally: promiseFianlly} = typedResult;
Object.setPrototypeOf(typedResult, Object.getPrototypeOf(root));
Object.defineProperties(typedResult, Object.getOwnPropertyDescriptors(root));

// These should point to the new promise
// eslint-disable-next-line promise/prefer-await-to-then
typedResult.then = promiseThen;
typedResult.catch = promiseCatch;
typedResult.finally = promiseFianlly;
}

return result;
}));

// Got interface
const got: Got = ((url: string | URL, options: Options = {}, _defaults?: Defaults): GotReturn => {
let iteration = 0;
const iterateHandlers = (newOptions: NormalizedOptions): GotReturn => {
return defaults.handlers[iteration++](
newOptions,
iteration === defaults.handlers.length ? getPromiseOrStream : iterateHandlers
) as GotReturn;
};

// TODO: Remove this in Got 12.
if (is.plainObject(url)) {
const mergedOptions = {
...url as Options,
...options
};

setNonEnumerableProperties([url as Options, options], mergedOptions);

options = mergedOptions;
url = undefined as any;
}

try {
// Call `init` hooks
let initHookError: Error | undefined;
try {
callInitHooks(defaults.options.hooks.init, options);
callInitHooks(options.hooks?.init, options);
} catch (error) {
initHookError = error;
}

// Normalize options & call handlers
const normalizedOptions = normalizeArguments(url, options, _defaults ?? defaults.options);
normalizedOptions[kIsNormalizedAlready] = true;

if (initHookError) {
throw new RequestError(initHookError.message, initHookError, normalizedOptions);
}

return iterateHandlers(normalizedOptions);
} catch (error) {
if (options.isStream) {
throw error;
} else {
return createRejection(error, defaults.options.hooks.beforeError, options.hooks?.beforeError);
}
}
}) as Got;

got.extend = (...instancesOrOptions) => {
const optionsArray: Options[] = [defaults.options];
let handlers: HandlerFunction[] = [...defaults._rawHandlers!];
let isMutableDefaults: boolean | undefined;

for (const value of instancesOrOptions) {
if (isGotInstance(value)) {
optionsArray.push(value.defaults.options);
handlers.push(...value.defaults._rawHandlers!);
isMutableDefaults = value.defaults.mutableDefaults;
} else {
optionsArray.push(value);

if ('handlers' in value) {
handlers.push(...value.handlers!);
}

isMutableDefaults = value.mutableDefaults;
}
}

handlers = handlers.filter(handler => handler !== defaultHandler);

if (handlers.length === 0) {
handlers.push(defaultHandler);
}

return create({
options: mergeOptions(...optionsArray),
handlers,
mutableDefaults: Boolean(isMutableDefaults)
});
};

// Pagination
const paginateEach = (async function * <T, R>(url: string | URL, options?: OptionsWithPagination<T, R>): AsyncIterableIterator<T> {
// TODO: Remove this `@ts-expect-error` when upgrading to TypeScript 4.
// Error: Argument of type 'Merge<Options, PaginationOptions<T, R>> | undefined' is not assignable to parameter of type 'Options | undefined'.
// @ts-expect-error
let normalizedOptions = normalizeArguments(url, options, defaults.options);
normalizedOptions.resolveBodyOnly = false;

const pagination = normalizedOptions.pagination!;

if (!is.object(pagination)) {
throw new TypeError('`options.pagination` must be implemented');
}

const all: T[] = [];
let {countLimit} = pagination;

let numberOfRequests = 0;
while (numberOfRequests < pagination.requestLimit) {
if (numberOfRequests !== 0) {
// eslint-disable-next-line no-await-in-loop
await delay(pagination.backoff);
}

// @ts-expect-error FIXME!
// TODO: Throw when result is not an instance of Response
// eslint-disable-next-line no-await-in-loop
const result = (await got(undefined, undefined, normalizedOptions)) as Response;

// eslint-disable-next-line no-await-in-loop
const parsed = await pagination.transform(result);
const current: T[] = [];

for (const item of parsed) {
if (pagination.filter(item, all, current)) {
if (!pagination.shouldContinue(item, all, current)) {
return;
}

yield item as T;

if (pagination.stackAllItems) {
all.push(item as T);
}

current.push(item as T);

if (--countLimit <= 0) {
return;
}
}
}

const optionsToMerge = pagination.paginate(result, all, current);

if (optionsToMerge === false) {
return;
}

if (optionsToMerge === result.request.options) {
normalizedOptions = result.request.options;
} else if (optionsToMerge !== undefined) {
normalizedOptions = normalizeArguments(undefined, optionsToMerge, normalizedOptions);
}

numberOfRequests++;
}
});

got.paginate = paginateEach as GotPaginate;

got.paginate.all = (async <T, R>(url: string | URL, options?: OptionsWithPagination<T, R>) => {
const results: T[] = [];

for await (const item of paginateEach<T, R>(url, options)) {
results.push(item);
}

return results;
}) as GotPaginate['all'];

// For those who like very descriptive names
got.paginate.each = paginateEach as GotPaginate['each'];

// Stream API
got.stream = ((url: string | URL, options?: StreamOptions) => got(url, {...options, isStream: true})) as GotStream;

// Shortcuts
for (const method of aliases) {
got[method] = ((url: string | URL, options?: Options): GotReturn => got(url, {...options, method})) as GotRequestFunction;

got.stream[method] = ((url: string | URL, options?: StreamOptions) => {
return got(url, {...options, method, isStream: true});
}) as GotStream;
}

Object.assign(got, errors);
Object.defineProperty(got, 'defaults', {
value: defaults.mutableDefaults ? defaults : deepFreeze(defaults),
writable: defaults.mutableDefaults,
configurable: defaults.mutableDefaults,
enumerable: true
});

got.mergeOptions = mergeOptions;

return got;
};

export default create;
export * from './types';
133 changes: 133 additions & 0 deletions source/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {URL} from 'url';
import {Response, Options} from './as-promise';
import create, {defaultHandler, InstanceDefaults} from './create';

const defaults: InstanceDefaults = {
options: {
method: 'GET',
retry: {
limit: 2,
methods: [
'GET',
'PUT',
'HEAD',
'DELETE',
'OPTIONS',
'TRACE'
],
statusCodes: [
408,
413,
429,
500,
502,
503,
504,
521,
522,
524
],
errorCodes: [
'ETIMEDOUT',
'ECONNRESET',
'EADDRINUSE',
'ECONNREFUSED',
'EPIPE',
'ENOTFOUND',
'ENETUNREACH',
'EAI_AGAIN'
],
maxRetryAfter: undefined,
calculateDelay: ({computedValue}) => computedValue
},
timeout: {},
headers: {
'user-agent': 'got (https://github.com/sindresorhus/got)'
},
hooks: {
init: [],
beforeRequest: [],
beforeRedirect: [],
beforeRetry: [],
beforeError: [],
afterResponse: []
},
cache: undefined,
dnsCache: undefined,
decompress: true,
throwHttpErrors: true,
followRedirect: true,
isStream: false,
responseType: 'text',
resolveBodyOnly: false,
maxRedirects: 10,
prefixUrl: '',
methodRewriting: true,
ignoreInvalidCookies: false,
context: {},
// TODO: Set this to `true` when Got 12 gets released
http2: false,
allowGetBody: false,
https: undefined,
pagination: {
transform: (response: Response) => {
if (response.request.options.responseType === 'json') {
return response.body;
}

return JSON.parse(response.body as string);
},
paginate: response => {
if (!Reflect.has(response.headers, 'link')) {
return false;
}

const items = (response.headers.link as string).split(',');

let next: string | undefined;
for (const item of items) {
const parsed = item.split(';');

if (parsed[1].includes('next')) {
next = parsed[0].trimStart().trim();
next = next.slice(1, -1);
break;
}
}

if (next) {
const options: Options = {
url: new URL(next)
};

return options;
}

return false;
},
filter: () => true,
shouldContinue: () => true,
countLimit: Infinity,
backoff: 0,
requestLimit: 10000,
stackAllItems: true
},
parseJson: (text: string) => JSON.parse(text),
stringifyJson: (object: unknown) => JSON.stringify(object),
cacheOptions: {}
},
handlers: [defaultHandler],
mutableDefaults: false
};

const got = create(defaults);

export default got;

// For CommonJS default export support
module.exports = got;
module.exports.default = got;
module.exports.__esModule = true; // Workaround for TS issue: https://github.com/sindresorhus/got/pull/1267

export * from './create';
export * from './as-promise';
391 changes: 391 additions & 0 deletions source/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,391 @@
import {URL} from 'url';
import {CancelError} from 'p-cancelable';
import {
// Request & Response
CancelableRequest,
Response,

// Options
Options,
NormalizedOptions,
Defaults as DefaultOptions,
PaginationOptions,

// Errors
ParseError,
RequestError,
CacheError,
ReadError,
HTTPError,
MaxRedirectsError,
TimeoutError,
UnsupportedProtocolError,
UploadError
} from './as-promise';
import Request from './core';

// `type-fest` utilities
type Except<ObjectType, KeysType extends keyof ObjectType> = Pick<ObjectType, Exclude<keyof ObjectType, KeysType>>;
type Merge<FirstType, SecondType> = Except<FirstType, Extract<keyof FirstType, keyof SecondType>> & SecondType;

/**
Defaults for each Got instance.
*/
export interface InstanceDefaults {
/**
An object containing the default options of Got.
*/
options: DefaultOptions;

/**
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` or `asStream`, depending on the `options.isStream` property.
@default []
*/
handlers: HandlerFunction[];

/**
A read-only boolean describing whether the defaults are mutable or not.
If set to `true`, you can update headers over time, for example, update an access token when it expires.
@default false
*/
mutableDefaults: boolean;

_rawHandlers?: HandlerFunction[];
}

/**
A Request object returned by calling Got, or any of the Got HTTP alias request functions.
*/
export type GotReturn = Request | CancelableRequest;

/**
A function to handle options and returns a Request object.
It acts sort of like a "global hook", and will be called before any actual request is made.
*/
export type HandlerFunction = <T extends GotReturn>(options: NormalizedOptions, next: (options: NormalizedOptions) => T) => T | Promise<T>;

/**
The options available for `got.extend()`.
*/
export interface ExtendOptions extends Options {
/**
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` or `asStream`, depending on the `options.isStream` property.
@default []
*/
handlers?: HandlerFunction[];

/**
A read-only boolean describing whether the defaults are mutable or not.
If set to `true`, you can update headers over time, for example, update an access token when it expires.
@default false
*/
mutableDefaults?: boolean;
}

export type OptionsOfTextResponseBody = Merge<Options, {isStream?: false; resolveBodyOnly?: false; responseType?: 'text'}>;
export type OptionsOfJSONResponseBody = Merge<Options, {isStream?: false; resolveBodyOnly?: false; responseType?: 'json'}>;
export type OptionsOfBufferResponseBody = Merge<Options, {isStream?: false; resolveBodyOnly?: false; responseType: 'buffer'}>;
export type OptionsOfUnknownResponseBody = Merge<Options, {isStream?: false; resolveBodyOnly?: false}>;
export type StrictOptions = Except<Options, 'isStream' | 'responseType' | 'resolveBodyOnly'>;
export type StreamOptions = Merge<Options, {isStream?: true}>;
type ResponseBodyOnly = {resolveBodyOnly: true};

export type OptionsWithPagination<T = unknown, R = unknown> = Merge<Options, PaginationOptions<T, R>>;

/**
An instance of `got.paginate`.
*/
export interface GotPaginate {
/**
Same as `GotPaginate.each`.
*/
<T, R = unknown>(url: string | URL, options?: OptionsWithPagination<T, R>): AsyncIterableIterator<T>;

/**
Same as `GotPaginate.each`.
*/
<T, R = unknown>(options?: OptionsWithPagination<T, R>): AsyncIterableIterator<T>;

/**
Returns an async iterator.
See pagination.options for more pagination options.
@example
```
(async () => {
const countLimit = 10;
const pagination = got.paginate('https://api.github.com/repos/sindresorhus/got/commits', {
pagination: {countLimit}
});
console.log(`Printing latest ${countLimit} Got commits (newest to oldest):`);
for await (const commitData of pagination) {
console.log(commitData.commit.message);
}
})();
```
*/
each: (<T, R = unknown>(url: string | URL, options?: OptionsWithPagination<T, R>) => AsyncIterableIterator<T>)
& (<T, R = unknown>(options?: OptionsWithPagination<T, R>) => AsyncIterableIterator<T>);

/**
Returns a Promise for an array of all results.
See pagination.options for more pagination options.
@example
```
(async () => {
const countLimit = 10;
const results = await got.paginate.all('https://api.github.com/repos/sindresorhus/got/commits', {
pagination: {countLimit}
});
console.log(`Printing latest ${countLimit} Got commits (newest to oldest):`);
console.log(results);
})();
```
*/
all: (<T, R = unknown>(url: string | URL, options?: OptionsWithPagination<T, R>) => Promise<T[]>)
& (<T, R = unknown>(options?: OptionsWithPagination<T, R>) => Promise<T[]>);
}

export interface GotRequestFunction {
// `asPromise` usage
(url: string | URL, options?: OptionsOfTextResponseBody): CancelableRequest<Response<string>>;
<T>(url: string | URL, options?: OptionsOfJSONResponseBody): CancelableRequest<Response<T>>;
(url: string | URL, options?: OptionsOfBufferResponseBody): CancelableRequest<Response<Buffer>>;
(url: string | URL, options?: OptionsOfUnknownResponseBody): CancelableRequest<Response>;

(options: OptionsOfTextResponseBody): CancelableRequest<Response<string>>;
<T>(options: OptionsOfJSONResponseBody): CancelableRequest<Response<T>>;
(options: OptionsOfBufferResponseBody): CancelableRequest<Response<Buffer>>;
(options: OptionsOfUnknownResponseBody): CancelableRequest<Response>;

// `resolveBodyOnly` usage
(url: string | URL, options?: (Merge<OptionsOfTextResponseBody, ResponseBodyOnly>)): CancelableRequest<string>;
<T>(url: string | URL, options?: (Merge<OptionsOfJSONResponseBody, ResponseBodyOnly>)): CancelableRequest<T>;
(url: string | URL, options?: (Merge<OptionsOfBufferResponseBody, ResponseBodyOnly>)): CancelableRequest<Buffer>;

(options: (Merge<OptionsOfTextResponseBody, ResponseBodyOnly>)): CancelableRequest<string>;
<T>(options: (Merge<OptionsOfJSONResponseBody, ResponseBodyOnly>)): CancelableRequest<T>;
(options: (Merge<OptionsOfBufferResponseBody, ResponseBodyOnly>)): CancelableRequest<Buffer>;

// `asStream` usage
(url: string | URL, options?: Merge<Options, {isStream: true}>): Request;

(options: Merge<Options, {isStream: true}>): Request;

// Fallback
(url: string | URL, options?: Options): CancelableRequest | Request;

(options: Options): CancelableRequest | Request;
}

/**
All available HTTP request methods provided by Got.
*/
export type HTTPAlias =
| 'get'
| 'post'
| 'put'
| 'patch'
| 'head'
| 'delete';

interface GotStreamFunction {
(url: string | URL, options?: Merge<Options, {isStream?: true}>): Request;
(options?: Merge<Options, {isStream?: true}>): Request;
}

/**
An instance of `got.stream()`.
*/
export type GotStream = GotStreamFunction & Record<HTTPAlias, GotStreamFunction>;

/**
An instance of `got`.
*/
export interface Got extends Record<HTTPAlias, GotRequestFunction>, GotRequestFunction {
/**
Sets `options.isStream` to `true`.
Returns a [duplex stream](https://nodejs.org/api/stream.html#stream_class_stream_duplex) with additional events:
- request
- response
- redirect
- uploadProgress
- downloadProgress
- error
*/
stream: GotStream;

/**
Returns an async iterator.
See pagination.options for more pagination options.
@example
```
(async () => {
const countLimit = 10;
const pagination = got.paginate('https://api.github.com/repos/sindresorhus/got/commits', {
pagination: {countLimit}
});
console.log(`Printing latest ${countLimit} Got commits (newest to oldest):`);
for await (const commitData of pagination) {
console.log(commitData.commit.message);
}
})();
```
*/
paginate: GotPaginate;

/**
The Got defaults used in that instance.
*/
defaults: InstanceDefaults;

/**
An error to be thrown when a cache method fails. For example, if the database goes down or there's a filesystem error.
Contains a `code` property with `ERR_CACHE_ACCESS` or a more specific failure code.
*/
CacheError: typeof CacheError;

/**
An error to be thrown when a request fails. Contains a `code` property with error class code, like `ECONNREFUSED`.
If there is no specific code supplied, `code` defaults to `ERR_GOT_REQUEST_ERROR`.
*/
RequestError: typeof RequestError;

/**
An error to be thrown when reading from response stream fails. Contains a `code` property with
`ERR_READING_RESPONSE_STREAM` or a more specific failure code.
*/
ReadError: typeof ReadError;

/**
An error to be thrown when server response code is 2xx, and parsing body fails. Includes a
`response` property. Contains a `code` property with `ERR_BODY_PARSE_FAILURE` or a more specific failure code.
*/
ParseError: typeof ParseError;

/**
An error to be thrown when the server response code is not 2xx nor 3xx if `options.followRedirect` is `true`, but always except for 304.
Includes a `response` property. Contains a `code` property with `ERR_NON_2XX_3XX_RESPONSE` or a more specific failure code.
*/
HTTPError: typeof HTTPError;

/**
An error to be thrown when the server redirects you more than ten times.
Includes a `response` property. Contains a `code` property with `ERR_TOO_MANY_REDIRECTS`.
*/
MaxRedirectsError: typeof MaxRedirectsError;

/**
An error to be thrown when given an unsupported protocol. Contains a `code` property with `ERR_UNSUPPORTED_PROTOCOL`.
*/
UnsupportedProtocolError: typeof UnsupportedProtocolError;

/**
An error to be thrown when the request is aborted due to a timeout.
Includes an `event` and `timings` property. Contains a `code` property with `ETIMEDOUT`.
*/
TimeoutError: typeof TimeoutError;

/**
An error to be thrown when the request body is a stream and an error occurs while reading from that stream.
Contains a `code` property with `ERR_UPLOAD` or a more specific failure code.
*/
UploadError: typeof UploadError;

/**
An error to be thrown when the request is aborted with `.cancel()`. Contains a `code` property with `ERR_CANCELED`.
*/
CancelError: typeof CancelError;

/**
Configure a new `got` instance with default `options`.
The `options` are merged with the parent instance's `defaults.options` using `got.mergeOptions`.
You can access the resolved options with the `.defaults` property on the instance.
Additionally, `got.extend()` accepts two properties from the `defaults` object: `mutableDefaults` and `handlers`.
It is also possible to merges many instances into a single one:
- options are merged using `got.mergeOptions()` (including hooks),
- handlers are stored in an array (you can access them through `instance.defaults.handlers`).
@example
```js
const client = got.extend({
prefixUrl: 'https://example.com',
headers: {
'x-unicorn': 'rainbow'
}
});
client.get('demo');
// HTTP Request =>
// GET /demo HTTP/1.1
// Host: example.com
// x-unicorn: rainbow
```
*/
extend: (...instancesOrOptions: Array<Got | ExtendOptions>) => Got;

/**
Merges multiple `got` instances into the parent.
*/
mergeInstances: (parent: Got, ...instances: Got[]) => Got;

/**
Extends parent options.
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.
Options are deeply merged to a new object. The value of each key is determined as follows:
- If the new property is not defined, the old value is used.
- If the new property is explicitly set to `undefined`:
- If the parent property is a plain `object`, the parent value is deeply cloned.
- Otherwise, `undefined` is used.
- If the parent value is an instance of `URLSearchParams`:
- If the new value is a `string`, an `object` or an instance of `URLSearchParams`, a new `URLSearchParams` instance is created.
The values are merged using [`urlSearchParams.append(key, value)`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/append).
The keys defined in the new value override the keys defined in the parent value.
- Otherwise, the only available value is `undefined`.
- If the new property is a plain `object`:
- If the parent property is a plain `object` too, both values are merged recursively into a new `object`.
- Otherwise, only the new value is deeply cloned.
- If the new property is an `Array`, it overwrites the old one with a deep clone of the new property.
- Properties that are not enumerable, such as `context`, `body`, `json`, and `form`, will not be merged.
- Otherwise, the new value is assigned to the key.
**Note:** Only Got options are merged! Custom user options should be defined via [`options.context`](#context).
@example
```
const a = {headers: {cat: 'meow', wolf: ['bark', 'wrrr']}};
const b = {headers: {cow: 'moo', wolf: ['auuu']}};
{...a, ...b} // => {headers: {cow: 'moo', wolf: ['auuu']}}
got.mergeOptions(a, b) // => {headers: {cat: 'meow', cow: 'moo', wolf: ['auuu']}}
```
*/
mergeOptions: (...sources: Options[]) => NormalizedOptions;
}
11 changes: 11 additions & 0 deletions source/utils/deep-freeze.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import is from '@sindresorhus/is';

export default function deepFreeze<T extends Record<string, any>>(object: T): Readonly<T> {
for (const value of Object.values(object)) {
if (is.plainObject(value) || is.array(value)) {
deepFreeze(value);
}
}

return Object.freeze(object);
}
14 changes: 14 additions & 0 deletions source/utils/deprecation-warning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const alreadyWarned: Set<string> = new Set();

export default (message: string) => {
if (alreadyWarned.has(message)) {
return;
}

alreadyWarned.add(message);

// @ts-expect-error Missing types.
process.emitWarning(`Got: ${message}`, {
type: 'DeprecationWarning'
});
};
Loading