├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── 1-bug-report.md │ ├── 2-feature-request.md │ └── 3-question.md ├── PULL_REQUEST_TEMPLATE.md ├── funding.yml ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── benchmark ├── index.ts └── server.ts ├── documentation ├── 1-promise.md ├── 10-instances.md ├── 2-options.md ├── 3-streams.md ├── 4-pagination.md ├── 5-https.md ├── 6-timeout.md ├── 7-retry.md ├── 8-errors.md ├── 9-hooks.md ├── async-stack-traces.md ├── cache.md ├── examples │ ├── advanced-creation.js │ ├── gh-got.js │ ├── h2c.js │ ├── pagination.js │ ├── runkit-example.js │ └── uppercase-headers.js ├── lets-make-a-plugin.md ├── migration-guides │ ├── axios.md │ ├── nodejs.md │ └── request.md ├── quick-start.md ├── tips.md └── typescript.md ├── license ├── media ├── logo.ai ├── logo.png ├── logo.sketch └── logo.svg ├── package.json ├── readme.md ├── source ├── as-promise │ ├── index.ts │ └── types.ts ├── core │ ├── calculate-retry-delay.ts │ ├── errors.ts │ ├── index.ts │ ├── options.ts │ ├── parse-link-header.ts │ ├── response.ts │ ├── timed-out.ts │ └── utils │ │ ├── get-body-size.ts │ │ ├── is-client-request.ts │ │ ├── is-form-data.ts │ │ ├── is-unix-socket-url.ts │ │ ├── options-to-url.ts │ │ ├── proxy-events.ts │ │ ├── unhandle.ts │ │ ├── url-to-options.ts │ │ └── weakable-map.ts ├── create.ts ├── index.ts └── types.ts ├── test ├── abort.ts ├── agent.ts ├── arguments.ts ├── cache.ts ├── cancel.ts ├── cookies.ts ├── create.ts ├── encoding.ts ├── error.ts ├── extend.types.ts ├── fixtures │ ├── ok │ └── stream-content-length ├── gzip.ts ├── headers.ts ├── helpers.ts ├── helpers │ ├── create-http-test-server.ts │ ├── create-https-test-server.ts │ ├── invalid-url.ts │ ├── slow-data-stream.ts │ ├── types.ts │ └── with-server.ts ├── hooks.ts ├── http.ts ├── https.ts ├── merge-instances.ts ├── normalize-arguments.ts ├── pagination.ts ├── parse-link-header.ts ├── post.ts ├── progress.ts ├── promise.ts ├── redirects.ts ├── response-parse.ts ├── retry.ts ├── stream.ts ├── timeout.ts ├── timings.ts ├── types │ ├── create-test-server │ │ └── index.d.ts │ ├── pem.ts │ └── slow-stream │ │ └── index.d.ts ├── unix-socket.ts ├── url-to-options.ts └── weakable-map.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.ai binary 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐞 Bug report" 3 | about: Something is not working as it should 4 | --- 5 | 6 | #### Describe the bug 7 | 8 | - Node.js version: 9 | - OS & version: 10 | 11 | 12 | 13 | #### Actual behavior 14 | 15 | ... 16 | 17 | #### Expected behavior 18 | 19 | ... 20 | 21 | #### Code to reproduce 22 | 23 | ```js 24 | ... 25 | ``` 26 | 27 | 34 | 35 | #### Checklist 36 | 37 | - [ ] I have read the documentation. 38 | - [ ] I have tried my code with the latest version of Node.js and Got. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "⭐ Feature request" 3 | about: Suggest an idea for Got 4 | --- 5 | 6 | #### What problem are you trying to solve? 7 | 8 | ... 9 | 10 | #### Describe the feature 11 | 12 | ... 13 | 14 | 15 | 16 | #### Checklist 17 | 18 | - [ ] I have read the documentation and made sure this feature doesn't already exist. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "❓ Question" 3 | about: Something is unclear or needs to be discussed 4 | --- 5 | 6 | #### What would you like to discuss? 7 | 8 | ... 9 | 10 | #### Checklist 11 | 12 | - [ ] I have read the documentation. 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Checklist 2 | 3 | - [ ] I have read the documentation. 4 | - [ ] I have included a pull request description of my changes. 5 | - [ ] I have included some tests. 6 | - [ ] If it's a new feature, I have included documentation updates in both the README and the types. 7 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [sindresorhus, szmarczak] 2 | tidelift: npm/got 3 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | permissions: 6 | contents: read 7 | jobs: 8 | test: 9 | name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }} 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | node-version: 15 | - 20 16 | os: 17 | # Ubuntu fails and I don't have time to look into it. PR welcome. 18 | # - ubuntu-latest 19 | - macos-latest 20 | # Windows fails and I don't have time to look into it. PR welcome. 21 | # - windows-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install 28 | - run: npm test 29 | # - uses: codecov/codecov-action@v3 30 | # if: matrix.os == 'ubuntu-latest' && matrix.node-version == 20 31 | # with: 32 | # fail_ci_if_error: true 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | coverage 4 | .nyc_output 5 | dist 6 | *.0x 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /benchmark/index.ts: -------------------------------------------------------------------------------- 1 | import https from 'node:https'; 2 | /// import axios from 'axios'; 3 | import Benchmark from 'benchmark'; 4 | import fetch from 'node-fetch'; 5 | import request from 'request'; 6 | import got from '../source/index.js'; 7 | import Request from '../source/core/index.js'; 8 | import Options, {type OptionsInit} from '../source/core/options.js'; 9 | 10 | // Configuration 11 | const httpsAgent = new https.Agent({ 12 | keepAlive: true, 13 | rejectUnauthorized: false, 14 | }); 15 | 16 | const url = new URL('https://127.0.0.1:8081'); 17 | const urlString = url.toString(); 18 | 19 | const gotOptions: OptionsInit & {isStream?: true} = { 20 | agent: { 21 | https: httpsAgent, 22 | }, 23 | https: { 24 | rejectUnauthorized: false, 25 | }, 26 | retry: { 27 | limit: 0, 28 | }, 29 | }; 30 | 31 | const normalizedGotOptions = new Options(url, gotOptions); 32 | 33 | const requestOptions = { 34 | // eslint-disable-next-line @typescript-eslint/naming-convention 35 | strictSSL: false, 36 | agent: httpsAgent, 37 | }; 38 | 39 | const fetchOptions = { 40 | agent: httpsAgent, 41 | }; 42 | 43 | /// const axiosOptions = { 44 | // url: urlString, 45 | // httpsAgent, 46 | // https: { 47 | // rejectUnauthorized: false, 48 | // }, 49 | // }; 50 | 51 | // const axiosStreamOptions: typeof axiosOptions & {responseType: 'stream'} = { 52 | // ...axiosOptions, 53 | // responseType: 'stream', 54 | // }; 55 | 56 | const httpsOptions = { 57 | https: { 58 | rejectUnauthorized: false, 59 | }, 60 | agent: httpsAgent, 61 | }; 62 | 63 | const suite = new Benchmark.Suite(); 64 | 65 | // Benchmarking 66 | suite.add('got - promise', { 67 | defer: true, 68 | async fn(deferred: {resolve: () => void}) { 69 | await got(url, gotOptions); 70 | deferred.resolve(); 71 | }, 72 | }).add('got - stream', { 73 | defer: true, 74 | async fn(deferred: {resolve: () => void}) { 75 | got.stream(url, gotOptions).resume().once('end', () => { 76 | deferred.resolve(); 77 | }); 78 | }, 79 | }).add('got - core', { 80 | defer: true, 81 | async fn(deferred: {resolve: () => void}) { 82 | const stream = new Request(url, gotOptions); 83 | void stream.flush(); 84 | stream.resume().once('end', () => { 85 | deferred.resolve(); 86 | }); 87 | }, 88 | }).add('got - core - normalized options', { 89 | defer: true, 90 | async fn(deferred: {resolve: () => void}) { 91 | const stream = new Request(undefined, undefined, normalizedGotOptions); 92 | void stream.flush(); 93 | stream.resume().once('end', () => { 94 | deferred.resolve(); 95 | }); 96 | }, 97 | }).add('request - callback', { 98 | defer: true, 99 | fn(deferred: {resolve: () => void}) { 100 | request(urlString, requestOptions, (error: Error) => { 101 | if (error) { 102 | throw error; 103 | } 104 | 105 | deferred.resolve(); 106 | }); 107 | }, 108 | }).add('request - stream', { 109 | defer: true, 110 | fn(deferred: {resolve: () => void}) { 111 | const stream = request(urlString, requestOptions); 112 | stream.resume(); 113 | stream.once('end', () => { 114 | deferred.resolve(); 115 | }); 116 | }, 117 | }).add('node-fetch - promise', { 118 | defer: true, 119 | async fn(deferred: {resolve: () => void}) { 120 | const response = await fetch(urlString, fetchOptions); 121 | await response.text(); 122 | 123 | deferred.resolve(); 124 | }, 125 | }).add('node-fetch - stream', { 126 | defer: true, 127 | async fn(deferred: {resolve: () => void}) { 128 | const {body} = await fetch(urlString, fetchOptions); 129 | 130 | body!.resume(); 131 | body!.once('end', () => { 132 | deferred.resolve(); 133 | }); 134 | }, 135 | }).add('axios - promise', { 136 | defer: true, 137 | async fn(deferred: {resolve: () => void}) { 138 | // Disabled until it has correct types. 139 | // await axios.request(axiosOptions); 140 | deferred.resolve(); 141 | }, 142 | }).add('axios - stream', { 143 | defer: true, 144 | async fn(deferred: {resolve: () => void}) { 145 | // Disabled until it has correct types. 146 | // const result = await axios.request(axiosStreamOptions); 147 | // const {data}: any = result; 148 | 149 | // data.resume(); 150 | // data.once('end', () => { 151 | // deferred.resolve(); 152 | // }); 153 | 154 | deferred.resolve(); 155 | }, 156 | }).add('https - stream', { 157 | defer: true, 158 | fn(deferred: {resolve: () => void}) { 159 | https.request(urlString, httpsOptions, response => { 160 | response.resume(); 161 | response.once('end', () => { 162 | deferred.resolve(); 163 | }); 164 | }).end(); 165 | }, 166 | }).on('cycle', (event: Benchmark.Event) => { 167 | console.log(String(event.target)); 168 | }).on('complete', function (this: any) { 169 | console.log(`Fastest is ${this.filter('fastest').map('name') as string}`); 170 | 171 | internalBenchmark(); 172 | }).run(); 173 | 174 | const internalBenchmark = (): void => { 175 | console.log(); 176 | 177 | const internalSuite = new Benchmark.Suite(); 178 | internalSuite.add('got - normalize options', { 179 | fn() { 180 | // eslint-disable-next-line no-new 181 | new Options(url, gotOptions); 182 | }, 183 | }).on('cycle', (event: Benchmark.Event) => { 184 | console.log(String(event.target)); 185 | }); 186 | 187 | internalSuite.run(); 188 | }; 189 | 190 | // Results (i7-7700k, CPU governor: performance): 191 | 192 | // H2O server: 193 | // got - promise x 2,846 ops/sec ±3.71% (74 runs sampled) 194 | // got - stream x 3,840 ops/sec ±1.97% (83 runs sampled) 195 | // got - core x 3,929 ops/sec ±2.31% (83 runs sampled) 196 | // got - core - normalized options x 4,483 ops/sec ±2.25% (80 runs sampled) 197 | // request - callback x 4,784 ops/sec ±4.25% (77 runs sampled) 198 | // request - stream x 5,138 ops/sec ±2.10% (80 runs sampled) 199 | // node-fetch - promise x 6,693 ops/sec ±4.56% (77 runs sampled) 200 | // node-fetch - stream x 7,332 ops/sec ±3.22% (80 runs sampled) 201 | // axios - promise x 5,365 ops/sec ±4.30% (74 runs sampled) 202 | // axios - stream x 7,424 ops/sec ±3.09% (80 runs sampled) 203 | // https - stream x 8,850 ops/sec ±2.77% (71 runs sampled) 204 | // Fastest is https - stream 205 | 206 | // got - normalize options x 73,484 ops/sec ±0.85% (95 runs sampled) 207 | -------------------------------------------------------------------------------- /benchmark/server.ts: -------------------------------------------------------------------------------- 1 | import type {AddressInfo} from 'node:net'; 2 | import https from 'node:https'; 3 | // @ts-expect-error No types 4 | import createCert from 'create-cert'; 5 | 6 | const keys = await createCert({days: 365, commonName: 'localhost'}); 7 | 8 | const server = https.createServer(keys, (_request, response) => { 9 | response.end('ok'); 10 | }).listen(8080, () => { 11 | const {port} = server.address() as AddressInfo; 12 | console.log(`Listening at https://localhost:${port}`); 13 | }); 14 | -------------------------------------------------------------------------------- /documentation/1-promise.md: -------------------------------------------------------------------------------- 1 | [> Back to homepage](../readme.md#documentation) 2 | 3 | ## Promise API 4 | 5 | Source code: [`source/as-promise/index.ts`](../source/as-promise/index.ts) 6 | 7 | The main Got function returns a [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise).\ 8 | Although in order to support cancelation, [`PCancelable`](https://github.com/sindresorhus/p-cancelable) is used instead of pure `Promise`. 9 | 10 | ### got(url: string | URL, options?: [OptionsInit](typescript.md#optionsinit), defaults?: [Options](2-options.md)) 11 | 12 | **Returns: Promise<[Response](response.md)>** 13 | 14 | The most common way is to pass the URL as the first argument, then the options as the second. 15 | 16 | ```js 17 | import got from 'got'; 18 | 19 | const {headers} = await got( 20 | 'https://httpbin.org/anything', 21 | { 22 | headers: { 23 | foo: 'bar' 24 | } 25 | } 26 | ).json(); 27 | ``` 28 | 29 | ### got(options: [OptionsInit](typescript.md#optionsinit)) 30 | 31 | **Returns: Promise<[Response](3-streams.md#response-1)>** 32 | 33 | Alternatively, you can pass only options containing a `url` property. 34 | 35 | ```js 36 | import got from 'got'; 37 | 38 | const {headers} = await got( 39 | { 40 | url: 'https://httpbin.org/anything', 41 | headers: { 42 | foo: 'bar' 43 | } 44 | } 45 | ).json(); 46 | ``` 47 | 48 | This is semantically the same as the first approach. 49 | 50 | ### `promise.json()` 51 | 52 | **Returns: `Promise`** 53 | 54 | A shortcut method that gives a Promise returning a JSON object. 55 | 56 | It is semantically the same as settings [`options.resolveBodyOnly`](2-options.md#resolvebodyonly) to `true` and [`options.responseType`](2-options.md#responsetype) to `'json'`. 57 | 58 | ### `promise.buffer()` 59 | 60 | **Returns: `Promise`** 61 | 62 | A shortcut method that gives a Promise returning a [Buffer](https://nodejs.org/api/buffer.html). 63 | 64 | It is semantically the same as settings [`options.resolveBodyOnly`](2-options.md#resolvebodyonly) to `true` and [`options.responseType`](2-options.md#responsetype) to `'buffer'`. 65 | 66 | ### `promise.text()` 67 | 68 | **Returns: `Promise`** 69 | 70 | A shortcut method that gives a Promise returning a string. 71 | 72 | It is semantically the same as settings [`options.resolveBodyOnly`](2-options.md#resolvebodyonly) to `true` and [`options.responseType`](2-options.md#responsetype) to `'text'`. 73 | 74 | ### `promise.cancel(reason?: string)` 75 | 76 | Cancels the request and optionally provide a reason. 77 | 78 | The cancellation is synchronous.\ 79 | Calling it after the promise has settled or multiple times does nothing. 80 | 81 | This will cause the promise to reject with [`CancelError`](8-errors.md#cancelerror). 82 | 83 | ### `promise.isCanceled` 84 | 85 | **Type: `boolean`** 86 | 87 | Whether the promise is canceled. 88 | 89 | ### `promise.on(event, handler)` 90 | 91 | The events are the same as in [Stream API](3-streams.md#events). 92 | 93 | ### `promise.off(event, handler)` 94 | 95 | Removes listener registered with [`promise.on`](1-promise.md#promiseonevent-handler) 96 | 97 | ```js 98 | import {createReadStream} from 'node:fs'; 99 | import got from 'got'; 100 | 101 | const ongoingRequestPromise = got.post(uploadUrl, { 102 | body: createReadStream('sample.txt') 103 | }); 104 | 105 | const eventListener = (progress: Progress) => { 106 | console.log(progress); 107 | }; 108 | 109 | ongoingRequestPromise.on('uploadProgress', eventListener); 110 | 111 | setTimeout(() => { 112 | ongoingRequestPromise.off('uploadProgress', eventListener); 113 | }, 500); 114 | 115 | await ongoingRequestPromise; 116 | ``` 117 | -------------------------------------------------------------------------------- /documentation/10-instances.md: -------------------------------------------------------------------------------- 1 | [> Back to homepage](../readme.md#documentation) 2 | 3 | ## Instances 4 | 5 | Source code: [`source/create.ts`](../source/create.ts) 6 | 7 | ### `got.defaults` 8 | 9 | #### `options` 10 | 11 | **Type: [`Options`](2-options.md)** 12 | 13 | The options used for this instance. 14 | 15 | #### `handlers` 16 | 17 | **Type: [`Handler[]`](typescript.md#handler)** 18 | 19 | ```ts 20 | (options: Options, next: …) => next(options) 21 | ``` 22 | 23 | An array of handlers. The `next` function returns a [`Promise`](1-promise.md) or a [`Request` Got stream](3-streams.md). 24 | 25 | You execute them directly by calling `got(…)`. They are some sort of "global hooks" - these functions are called first. The last handler (it's invisible) is either `asPromise` or `asStream`, depending on the `options.isStream` property. 26 | 27 | #### `mutableDefaults` 28 | 29 | **Type: `boolean`**\ 30 | **Default: `false`** 31 | 32 | Determines whether `got.defaults.options` can be modified. 33 | 34 | ### `got.extend(…options, …instances)` 35 | 36 | **Tip:** 37 | > - `options` can include `handlers` and `mutableDefaults`. 38 | 39 | **Note:** 40 | > - Properties that are not enumerable, such as `body`, `json`, and `form`, will not be merged. 41 | 42 | Configure a new `got` instance with merged default options. The options are merged with the parent instance's `defaults.options` using [`options.merge(…)`](2-options.md#merge). 43 | 44 | ```js 45 | import got from 'got'; 46 | 47 | const client = got.extend({ 48 | prefixUrl: 'https://httpbin.org', 49 | headers: { 50 | 'x-foo': 'bar' 51 | } 52 | }); 53 | 54 | const {headers} = await client.get('headers').json(); 55 | console.log(headers['x-foo']); //=> 'bar' 56 | 57 | const jsonClient = client.extend({ 58 | responseType: 'json', 59 | resolveBodyOnly: true, 60 | headers: { 61 | 'x-lorem': 'impsum' 62 | } 63 | }); 64 | 65 | const {headers: headers2} = await jsonClient.get('headers'); 66 | console.log(headers2['x-foo']); //=> 'bar' 67 | console.log(headers2['x-lorem']); //=> 'impsum' 68 | ``` 69 | 70 | **Note:** 71 | > - Handlers can be asynchronous and can return a `Promise`, but never a `Promise` if `options.isStream` is `true`. 72 | > - Streams must always be handled synchronously. 73 | > - In order to perform async work using streams, the `beforeRequest` hook should be used instead. 74 | 75 | The recommended approach for creating handlers that can handle both promises and streams is: 76 | 77 | ```js 78 | import got from 'got'; 79 | 80 | // Create a non-async handler, but we can return a Promise later. 81 | const handler = (options, next) => { 82 | if (options.isStream) { 83 | // It's a Stream, return synchronously. 84 | return next(options); 85 | } 86 | 87 | // For asynchronous work, return a Promise. 88 | return (async () => { 89 | try { 90 | const response = await next(options); 91 | response.yourOwnProperty = true; 92 | return response; 93 | } catch (error) { 94 | // Every error will be replaced by this one. 95 | // Before you receive any error here, 96 | // it will be passed to the `beforeError` hooks first. 97 | // Note: this one won't be passed to `beforeError` hook. It's final. 98 | throw new Error('Your very own error.'); 99 | } 100 | })(); 101 | }; 102 | 103 | const instance = got.extend({handlers: [handler]}); 104 | ``` 105 | -------------------------------------------------------------------------------- /documentation/4-pagination.md: -------------------------------------------------------------------------------- 1 | [> Back to homepage](../readme.md#documentation) 2 | 3 | ## Pagination API 4 | 5 | Source code: [`source/create.ts`](../source/create.ts) 6 | 7 | ### How does the `Link` header work? 8 | 9 | The [RFC5988](https://datatracker.ietf.org/doc/html/rfc5988#section-5) defines how the `Link` header looks like. 10 | 11 | When the response has been processed, Got looks for [the reference of the `next` relation](https://datatracker.ietf.org/doc/html/rfc5988#section-6.2.2).\ 12 | This way Got knows the URL it should visit afterwards. The header can look like this: 13 | 14 | ``` 15 | Link: ; rel="next", ; rel="last" 16 | ``` 17 | 18 | By default, Got looks only at the `next` relation. To use [other relations](https://datatracker.ietf.org/doc/html/rfc5988#section-6.2.2), you need to customize the `paginate` function below. 19 | 20 | ### `got.paginate(url, options?)` 21 | ### `got.paginate.each(url, options?)` 22 | 23 | Returns an [async iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of). 24 | 25 | This is memory efficient, as the logic is executed immediately when new data comes in. 26 | 27 | ```js 28 | import got from 'got'; 29 | 30 | const countLimit = 10; 31 | 32 | const pagination = got.paginate( 33 | 'https://api.github.com/repos/sindresorhus/got/commits', 34 | { 35 | pagination: {countLimit} 36 | } 37 | ); 38 | 39 | console.log(`Printing latest ${countLimit} Got commits (newest to oldest):`); 40 | 41 | for await (const commitData of pagination) { 42 | console.log(commitData.commit.message); 43 | } 44 | ``` 45 | 46 | ### `got.paginate.all(url, options?)` 47 | 48 | **Note:** 49 | > - Querying a large dataset significantly increases memory usage. 50 | 51 | Returns a Promise for an array of all results. 52 | 53 | ```js 54 | import got from 'got'; 55 | 56 | const countLimit = 10; 57 | 58 | const results = await got.paginate.all('https://api.github.com/repos/sindresorhus/got/commits', { 59 | pagination: {countLimit} 60 | }); 61 | 62 | console.log(`Printing latest ${countLimit} Got commits (newest to oldest):`); 63 | console.log(results); 64 | ``` 65 | 66 | ### `pagination` 67 | 68 | **Type: `object`**\ 69 | **Default:** 70 | 71 | ```js 72 | { 73 | transform: (response: Response) => { 74 | if (response.request.options.responseType === 'json') { 75 | return response.body; 76 | } 77 | 78 | return JSON.parse(response.body as string); 79 | }, 80 | paginate: ({response}) => { 81 | const rawLinkHeader = response.headers.link; 82 | if (typeof rawLinkHeader !== 'string' || rawLinkHeader.trim() === '') { 83 | return false; 84 | } 85 | 86 | const parsed = parseLinkHeader(rawLinkHeader); 87 | const next = parsed.find(entry => entry.parameters.rel === 'next' || entry.parameters.rel === '"next"'); 88 | 89 | if (next) { 90 | return { 91 | url: new URL(next.reference, response.requestUrl) 92 | }; 93 | } 94 | 95 | return false; 96 | }, 97 | filter: () => true, 98 | shouldContinue: () => true, 99 | countLimit: Number.POSITIVE_INFINITY, 100 | backoff: 0, 101 | requestLimit: 10_000, 102 | stackAllItems: false 103 | } 104 | ``` 105 | 106 | This option represents the `pagination` object. 107 | 108 | #### `transform` 109 | 110 | **Type: `Function`**\ 111 | **Default: `response => JSON.parse(response.body)`** 112 | 113 | A function that transforms [`Response`](3-streams.md#response-1) into an array of items.\ 114 | This is where you should do the parsing. 115 | 116 | #### `paginate` 117 | 118 | **Type: `Function`**\ 119 | **Default: `Link` header logic** 120 | 121 | The function takes an object with the following properties: 122 | 123 | - `response` - The current response object, 124 | - `currentItems` - Items from the current response, 125 | - `allItems` - An empty array, unless `stackAllItems` is `true`, otherwise it contains all emitted items. 126 | 127 | It should return an object representing Got options pointing to the next page. If there is no next page, `false` should be returned instead. 128 | 129 | The options are merged automatically with the previous request.\ 130 | Therefore the options returned by `pagination.paginate(…)` must reflect changes only. 131 | 132 | **Note:** 133 | > - The `url` option (if set) accepts **only** a [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) instance.\ 134 | > This prevents `prefixUrl` ambiguity. In order to use a relative URL string, merge it via `new URL(relativeUrl, response.url)`. 135 | 136 | #### `filter` 137 | 138 | **Type: `Function`**\ 139 | **Default: `({item, currentItems, allItems}) => true`** 140 | 141 | Whether the item should be emitted or not. 142 | 143 | #### `shouldContinue` 144 | 145 | **Type: `Function`**\ 146 | **Default: `({item, currentItems, allItems}) => true`** 147 | 148 | **Note:** 149 | > - This function executes only when `filter` returns `true`. 150 | 151 | For example, if you need to stop before emitting an entry with some flag, you should use `({item}) => !item.flag`. 152 | 153 | If you want to stop after emitting the entry, you should use `({item, allItems}) => allItems.some(item => item.flag)` instead. 154 | 155 | #### `countLimit` 156 | 157 | **Type: `number`**\ 158 | **Default: `Number.POSITIVE_INFINITY`** 159 | 160 | The maximum amount of items that should be emitted. 161 | 162 | #### `backoff` 163 | 164 | **Type: `number`**\ 165 | **Default: `0`** 166 | 167 | Milliseconds to wait before the next request is triggered. 168 | 169 | #### `requestLimit` 170 | 171 | **Type: `number`**\ 172 | **Default: `10000`** 173 | 174 | The maximum amount of request that should be triggered. 175 | 176 | **Note:** 177 | > - [Retries on failure](7-retry.md) are not counted towards this limit. 178 | 179 | #### `stackAllItems` 180 | 181 | **Type: `boolean`**\ 182 | **Default: `false`** 183 | 184 | Defines how `allItems` is managed in `pagination.paginate`, `pagination.filter` and `pagination.shouldContinue`. 185 | 186 | By default, `allItems` is always an empty array. Setting this to `true` will significantly increase memory usage when working with a large dataset. 187 | 188 | ### Example 189 | 190 | In this example we will use `searchParams` instead of `Link` header.\ 191 | Just to show how you can customize the `paginate` function. 192 | 193 | The reason `filter` looks exactly the same like `shouldContinue` is that the latter will tell Got to stop once we reach our timestamp. 194 | The `filter` function is needed as well, because in the same response we can get results with different timestamps. 195 | 196 | ```js 197 | import got from 'got'; 198 | import Bourne from '@hapi/bourne'; 199 | 200 | const max = Date.now() - 1000 * 86400 * 7; 201 | 202 | const iterator = got.paginate('https://api.github.com/repos/sindresorhus/got/commits', { 203 | pagination: { 204 | paginate: ({response, currentItems}) => { 205 | // If there are no more data, finish. 206 | if (currentItems.length === 0) { 207 | return false; 208 | } 209 | 210 | // Get the current page number. 211 | const {searchParams} = response.request.options; 212 | const previousPage = Number(searchParams.get('page') ?? 1); 213 | 214 | // Update the page number by one. 215 | return { 216 | searchParams: { 217 | page: previousPage + 1 218 | } 219 | }; 220 | }, 221 | // Using `Bourne` to prevent prototype pollution. 222 | transform: response => Bourne.parse(response.body), 223 | filter: ({item}) => { 224 | // Check if the commit time exceeds our range. 225 | const date = new Date(item.commit.committer.date); 226 | const end = date.getTime() - max >= 0; 227 | 228 | return end; 229 | }, 230 | shouldContinue: ({item}) => { 231 | // Check if the commit time exceeds our range. 232 | const date = new Date(item.commit.committer.date); 233 | const end = date.getTime() - max >= 0; 234 | 235 | return end; 236 | }, 237 | // We want only 50 results. 238 | countLimit: 50, 239 | // Wait 1s before making another request to prevent API rate limiting. 240 | backoff: 1000, 241 | // It is a good practice to set an upper limit of how many requests can be made. 242 | // This way we can avoid infinite loops. 243 | requestLimit: 10, 244 | // In this case, we don't need to store all the items we receive. 245 | // They are processed immediately. 246 | stackAllItems: false 247 | } 248 | }); 249 | 250 | console.log('Last 50 commits from now to week ago:'); 251 | for await (const item of iterator) { 252 | console.log(item.commit.message.split('\n')[0]); 253 | } 254 | ``` 255 | -------------------------------------------------------------------------------- /documentation/5-https.md: -------------------------------------------------------------------------------- 1 | [> Back to homepage](../readme.md#documentation) 2 | 3 | ## Advanced HTTPS API 4 | 5 | ### `https` 6 | 7 | **Type: `object`** 8 | 9 | This option represents the options used to make HTTPS requests. 10 | 11 | #### `alpnProtocols` 12 | 13 | **Type: `string[]`**\ 14 | **Default: `['http/1.1']`** 15 | 16 | Acceptable [ALPN](https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation) protocols. 17 | 18 | If the `http2` option is `true`, this defaults to `['h2', 'http/1.1']`. 19 | 20 | #### `rejectUnauthorized` 21 | 22 | **Type: `boolean`**\ 23 | **Default: `true`** 24 | 25 | If `true`, it will throw on invalid certificates, such as expired or self-signed ones. 26 | 27 | #### `checkServerIdentity` 28 | 29 | **Type: `(hostname: string, certificate: DetailedPeerCertificate) => Error | undefined`**\ 30 | **Default: `tls.checkServerIdentity`** 31 | 32 | Custom check of the certificate. Useful for pinning certificates. 33 | 34 | The function must return `undefined` if the check succeeded.\ 35 | If it failed, an `Error` should be returned. 36 | 37 | **Note:** 38 | > - In order to have the function called, the certificate must not be expired, self-signed nor with an untrusted-root. 39 | 40 | Check [Node.js docs](https://nodejs.org/api/https.html#https_https_request_url_options_callback) for an example. 41 | 42 | #### `certificateAuthority` 43 | 44 | **Type: `string | Buffer | string[] | Buffer[]`** 45 | 46 | **Note:** 47 | > - The option has been renamed from the [`ca` TLS option](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) for better readability. 48 | 49 | Overrides trusted [CA](https://en.wikipedia.org/wiki/Certificate_authority) certificates. 50 | 51 | Defaults to CAs provided by [Mozilla](https://ccadb-public.secure.force.com/mozilla/IncludedCACertificateReport). 52 | 53 | ```js 54 | import got from 'got'; 55 | 56 | // Single Certificate Authority 57 | await got('https://example.com', { 58 | https: { 59 | certificateAuthority: fs.readFileSync('./my_ca.pem') 60 | } 61 | }); 62 | ``` 63 | 64 | #### `key` 65 | 66 | **Type: `string | Buffer | string[] | Buffer[] | object[]`** 67 | 68 | Private keys in [PEM format](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail). 69 | 70 | Multiple keys with different passphrases can be provided as an array of `{pem: , passphrase: }`. 71 | 72 | **Note:** 73 | > - Encrypted keys will be decrypted with `https.passphrase`. 74 | 75 | #### `passphrase` 76 | 77 | **Type: `string`** 78 | 79 | Shared passphrase used for a single private key and/or a PFX. 80 | 81 | #### `certificate` 82 | 83 | **Type: `string | Buffer | string[] | Buffer[]`** 84 | 85 | **Note:** 86 | > - The option has been renamed from the [`cert` TLS option](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) for better readability. 87 | 88 | [Certificate chains](https://en.wikipedia.org/wiki/X.509#Certificate_chains_and_cross-certification) in [PEM format](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail). 89 | 90 | One certificate chain should be provided per private key. 91 | 92 | When providing multiple certificate chains, they do not have to be in the same order as their private keys in `https.key`. 93 | 94 | #### `pfx` 95 | 96 | **Type: `string | Buffer | string[] | Buffer[] | object[]`** 97 | 98 | [PFX or PKCS12](https://en.wikipedia.org/wiki/PKCS_12) encoded private key and certificate chain. Using `https.pfx` is an alternative to providing `https.key` and `https.certificate` individually. A PFX is usually encrypted, then `https.passphrase` will be used to decrypt it. 99 | 100 | Multiple PFX can be be provided as an array of unencrypted buffers or an array of objects like: 101 | 102 | ```ts 103 | { 104 | buffer: string | Buffer, 105 | passphrase?: string 106 | } 107 | ``` 108 | 109 | #### `certificateRevocationLists` 110 | 111 | **Type: `string | Buffer | string[] | Buffer[]`** 112 | 113 | **Note:** 114 | > - The option has been renamed from the [`crl` TLS option](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) for better readability. 115 | 116 | ### Other HTTPS options 117 | 118 | [Documentation for the below options.](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) 119 | 120 | - `ciphers` 121 | - `dhparam` 122 | - `signatureAlgorithms` (renamed from `sigalgs`) 123 | - `minVersion` 124 | - `maxVersion` 125 | - `honorCipherOrder` 126 | - `tlsSessionLifetime` (renamed from `sessionTimeout`) 127 | - `ecdhCurve` 128 | 129 | ### Examples 130 | 131 | ```js 132 | import got from 'got'; 133 | 134 | // Single key with certificate 135 | await got('https://example.com', { 136 | https: { 137 | key: fs.readFileSync('./client_key.pem'), 138 | certificate: fs.readFileSync('./client_cert.pem') 139 | } 140 | }); 141 | 142 | // Multiple keys with certificates (out of order) 143 | await got('https://example.com', { 144 | https: { 145 | key: [ 146 | fs.readFileSync('./client_key1.pem'), 147 | fs.readFileSync('./client_key2.pem') 148 | ], 149 | certificate: [ 150 | fs.readFileSync('./client_cert2.pem'), 151 | fs.readFileSync('./client_cert1.pem') 152 | ] 153 | } 154 | }); 155 | 156 | // Single key with passphrase 157 | await got('https://example.com', { 158 | https: { 159 | key: fs.readFileSync('./client_key.pem'), 160 | certificate: fs.readFileSync('./client_cert.pem'), 161 | passphrase: 'client_key_passphrase' 162 | } 163 | }); 164 | 165 | // Multiple keys with different passphrases 166 | await got('https://example.com', { 167 | https: { 168 | key: [ 169 | {pem: fs.readFileSync('./client_key1.pem'), passphrase: 'passphrase1'}, 170 | {pem: fs.readFileSync('./client_key2.pem'), passphrase: 'passphrase2'}, 171 | ], 172 | certificate: [ 173 | fs.readFileSync('./client_cert1.pem'), 174 | fs.readFileSync('./client_cert2.pem') 175 | ] 176 | } 177 | }); 178 | 179 | // Single encrypted PFX with passphrase 180 | await got('https://example.com', { 181 | https: { 182 | pfx: fs.readFileSync('./fake.pfx'), 183 | passphrase: 'passphrase' 184 | } 185 | }); 186 | 187 | // Multiple encrypted PFX's with different passphrases 188 | await got('https://example.com', { 189 | https: { 190 | pfx: [ 191 | { 192 | buffer: fs.readFileSync('./key1.pfx'), 193 | passphrase: 'passphrase1' 194 | }, 195 | { 196 | buffer: fs.readFileSync('./key2.pfx'), 197 | passphrase: 'passphrase2' 198 | } 199 | ] 200 | } 201 | }); 202 | 203 | // Multiple encrypted PFX's with single passphrase 204 | await got('https://example.com', { 205 | https: { 206 | passphrase: 'passphrase', 207 | pfx: [ 208 | { 209 | buffer: fs.readFileSync('./key1.pfx') 210 | }, 211 | { 212 | buffer: fs.readFileSync('./key2.pfx') 213 | } 214 | ] 215 | } 216 | }); 217 | ``` 218 | -------------------------------------------------------------------------------- /documentation/6-timeout.md: -------------------------------------------------------------------------------- 1 | [> Back to homepage](../readme.md#documentation) 2 | 3 | ## Timeout options 4 | 5 | Source code: [`source/core/timed-out.ts`](../source/core/timed-out.ts) 6 | 7 | It is a good practice to set a timeout to prevent hanging requests.\ 8 | By default, there is no timeout set. 9 | 10 | Note: The [`retry` configuration](7-retry.md) also applies to timeouts. 11 | 12 | **All numbers refer to milliseconds.** 13 | 14 | ```js 15 | import got from 'got'; 16 | 17 | const {timings} = await got('https://example.com', { 18 | timeout: { 19 | lookup: 100, 20 | connect: 50, 21 | secureConnect: 50, 22 | socket: 1000, 23 | send: 10000, 24 | response: 1000 25 | } 26 | }); 27 | 28 | // Alternatively: 29 | const {timings} = await got('https://example.com', { 30 | timeout: { 31 | request: 10000 32 | } 33 | }); 34 | 35 | console.log(timings); 36 | // { 37 | // start: 1625474926602, 38 | // socket: 1625474926605, 39 | // lookup: 1625474926610, 40 | // connect: 1625474926617, 41 | // secureConnect: 1625474926631, 42 | // upload: 1625474926631, 43 | // response: 1625474926638, 44 | // end: 1625474926642, 45 | // error: undefined, 46 | // abort: undefined, 47 | // phases: { 48 | // wait: 3, 49 | // dns: 5, 50 | // tcp: 7, 51 | // tls: 14, 52 | // request: 0, 53 | // firstByte: 7, 54 | // download: 4, 55 | // total: 40 56 | // } 57 | // } 58 | ``` 59 | 60 | ### `timeout` 61 | 62 | **Type: `object`** 63 | 64 | This object describes the maximum allowed time for particular events. 65 | 66 | #### `lookup` 67 | 68 | **Type: `number`** 69 | 70 | Starts when a socket is assigned.\ 71 | Ends when the hostname has been resolved. 72 | 73 | Does not apply when using a Unix domain socket.\ 74 | Does not apply when passing an IP address. 75 | 76 | It is preferred to not use any greater value than `100`. 77 | 78 | #### `connect` 79 | 80 | **Type: `number`** 81 | 82 | Starts when lookup completes.\ 83 | Ends when the socket is fully connected. 84 | 85 | If `lookup` does not apply to the request, this event starts when the socket is assigned and ends when the socket is connected. 86 | 87 | #### `secureConnect` 88 | 89 | **Type: `number`** 90 | 91 | Starts when `connect` completes.\ 92 | Ends when the handshake process completes. 93 | 94 | This timeout applies only to HTTPS requests. 95 | 96 | #### `socket` 97 | 98 | **Type: `number`** 99 | 100 | Starts when the socket is connected.\ 101 | Resets when new data is transferred. 102 | 103 | It is the same as [`request.setTimeout(timeout)`](https://nodejs.org/api/http.html#http_request_settimeout_timeout_callback) which calls [`socket.setTimeout(timeout)`](https://nodejs.org/api/net.html#socketsettimeouttimeout-callback) after a socket is assigned to this request and is connected. 104 | 105 | #### `send` 106 | 107 | **Type: `number`** 108 | 109 | Starts when the socket is connected.\ 110 | Ends when all data have been written to the socket. 111 | 112 | **Note:** 113 | > - This does not assure the data have been received by the other end! 114 | > - It only assures that the data have been passed to the underlying OS. 115 | 116 | #### `response` 117 | 118 | **Type: `number`** 119 | 120 | Starts when request has been flushed.\ 121 | Ends when the headers are received. 122 | 123 | #### ~~`read`~~ 124 | 125 | **Type: `number`** 126 | 127 | Starts when the headers are received.\ 128 | Ends when the response's `end` event fires. 129 | 130 | **Note:** 131 | > - This timeout is blocked by https://github.com/nodejs/node/issues/35923 132 | 133 | #### `request` 134 | 135 | **Type: `number`** 136 | 137 | Starts when the request is initiated.\ 138 | Ends when the response's `end` event fires. 139 | 140 | In other words, this is the global timeout. 141 | -------------------------------------------------------------------------------- /documentation/7-retry.md: -------------------------------------------------------------------------------- 1 | [> Back to homepage](../readme.md#documentation) 2 | 3 | ## Retry API 4 | 5 | **Note:** 6 | > If you're looking for retry implementation using streams, check out the [Retry Stream API](3-streams.md#retry). 7 | 8 | **Tip:** 9 | > You can trigger a retry by throwing the [`RetryError`](8-errors.md#retryerror) in any hook. 10 | 11 | **Tip:** 12 | > The `afterResponse` hook exposes a dedicated function to retry with merged options. [Read more](9-hooks.md#afterresponse). 13 | 14 | ### `retry` 15 | 16 | **Type: `object`**\ 17 | **Default:** 18 | 19 | ```js 20 | { 21 | limit: 2, 22 | methods: [ 23 | 'GET', 24 | 'PUT', 25 | 'HEAD', 26 | 'DELETE', 27 | 'OPTIONS', 28 | 'TRACE' 29 | ], 30 | statusCodes: [ 31 | 408, 32 | 413, 33 | 429, 34 | 500, 35 | 502, 36 | 503, 37 | 504, 38 | 521, 39 | 522, 40 | 524 41 | ], 42 | errorCodes: [ 43 | 'ETIMEDOUT', 44 | 'ECONNRESET', 45 | 'EADDRINUSE', 46 | 'ECONNREFUSED', 47 | 'EPIPE', 48 | 'ENOTFOUND', 49 | 'ENETUNREACH', 50 | 'EAI_AGAIN' 51 | ], 52 | maxRetryAfter: undefined, 53 | calculateDelay: ({computedValue}) => computedValue, 54 | backoffLimit: Number.POSITIVE_INFINITY, 55 | noise: 100 56 | } 57 | ``` 58 | 59 | This option represents the `retry` object. 60 | 61 | #### `limit` 62 | 63 | **Type: `number`** 64 | 65 | The maximum retry count. 66 | 67 | #### `methods` 68 | 69 | **Type: `string[]`** 70 | 71 | The allowed methods to retry on. 72 | 73 | **Note:** 74 | > - By default, Got does not retry on `POST`. 75 | 76 | #### `statusCodes` 77 | 78 | **Type: `number[]`** 79 | 80 | **Note:** 81 | > - Only [**unsuccessful**](8-errors.md#) requests are retried. In order to retry successful requests, use an [`afterResponse`](9-hooks.md#afterresponse) hook. 82 | 83 | The allowed [HTTP status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) to retry on. 84 | 85 | #### `errorCodes` 86 | 87 | **Type: `string[]`** 88 | 89 | The allowed error codes to retry on. 90 | 91 | - `ETIMEDOUT` - One of the [timeout limits](6-timeout.md) was reached. 92 | - `ECONNRESET`- The connection was forcibly closed. 93 | - `EADDRINUSE`- Could not bind to any free port. 94 | - `ECONNREFUSED`- The connection was refused by the server. 95 | - `EPIPE` - The remote side of the stream being written has been closed. 96 | - `ENOTFOUND` - Could not resolve the hostname to an IP address. 97 | - `ENETUNREACH` - No internet connection. 98 | - `EAI_AGAIN` - DNS lookup timed out. 99 | 100 | #### `maxRetryAfter` 101 | 102 | **Type: `number | undefined`**\ 103 | **Default: `options.timeout.request`** 104 | 105 | The upper limit of [`retry-after` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After). If `undefined`, it will use `options.timeout` as the value. 106 | 107 | If the limit is exceeded, the request is canceled. 108 | 109 | #### `calculateDelay` 110 | 111 | **Type: `Function`** 112 | 113 | ```ts 114 | (retryObject: RetryObject) => Promisable 115 | ``` 116 | 117 | ```ts 118 | interface RetryObject { 119 | attemptCount: number; 120 | retryOptions: RetryOptions; 121 | error: RequestError; 122 | computedValue: number; 123 | retryAfter?: number; 124 | } 125 | ``` 126 | 127 | The function used to calculate the delay before the next request is made. Returning `0` cancels the retry. 128 | 129 | **Note:** 130 | > - This function is responsible for the entire retry mechanism, including the `limit` property. To support this, you need to check if `computedValue` is different than `0`. 131 | 132 | **Tip:** 133 | > - This is especially useful when you want to scale down the computed value. 134 | 135 | ```js 136 | import got from 'got'; 137 | 138 | await got('https://httpbin.org/anything', { 139 | retry: { 140 | calculateDelay: ({computedValue}) => { 141 | return computedValue / 10; 142 | } 143 | } 144 | }); 145 | ``` 146 | 147 | #### `backoffLimit` 148 | 149 | **Type: `number`** 150 | 151 | The upper limit of the `computedValue`. 152 | 153 | By default, the `computedValue` is calculated in the following way: 154 | 155 | ```ts 156 | ((2 ** (attemptCount - 1)) * 1000) + noise 157 | ``` 158 | 159 | The delay increases exponentially.\ 160 | In order to prevent this, you can set this value to a fixed value, such as `1000`. 161 | 162 | #### `noise` 163 | 164 | **Type: `number`** 165 | 166 | The maximum acceptable retry noise in the range of `-100` to `+100`. 167 | -------------------------------------------------------------------------------- /documentation/8-errors.md: -------------------------------------------------------------------------------- 1 | [> Back to homepage](../readme.md#documentation) 2 | 3 | ## Errors 4 | 5 | Source code: 6 | - [`source/core/errors.ts`](../source/core/errors.ts) 7 | - [`source/as-promise/types.ts`](../source/as-promise/types.ts) 8 | - [`source/core/response.ts`](../source/core/response.ts) 9 | 10 | All Got errors contain various metadata, such as: 11 | 12 | - `code` - A string like `ERR_NON_2XX_3XX_RESPONSE`, 13 | - `options` - An instance of [`Options`](2-options.md), 14 | - `request` - An instance of Got Stream, 15 | - `response` (optional) - An instance of Got Response, 16 | - `timings` (optional) - Points to `response.timings`. 17 | 18 | #### Capturing async stack traces 19 | 20 | Read the article [here](async-stack-traces.md). 21 | 22 | > [!NOTE] 23 | > - The error codes may differ when the root error has a `code` property set. 24 | > - The root error will be propagated as is via the [`cause`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) property. 25 | 26 | ### `RequestError` 27 | 28 | **Code: `ERR_GOT_REQUEST_ERROR`** 29 | 30 | When a request fails. Contains a `code` property with error class code, like `ECONNREFUSED`. All the errors below inherit this one. 31 | 32 | ### `CacheError` 33 | 34 | **Code: `ERR_CACHE_ACCESS`** 35 | 36 | When a cache method fails, for example, if the database goes down or there's a filesystem error. 37 | 38 | ### `ReadError` 39 | 40 | **Code: `ERR_READING_RESPONSE_STREAM`** 41 | 42 | When reading from response stream fails. 43 | 44 | ### `ParseError` 45 | 46 | **Code: `ERR_BODY_PARSE_FAILURE`** 47 | 48 | When server response code is 2xx, and parsing body fails. Includes a `response` property. 49 | 50 | ### `UploadError` 51 | 52 | **Code: `ERR_UPLOAD`** 53 | 54 | When the request body is a stream and an error occurs while reading from that stream. 55 | 56 | ### `HTTPError` 57 | 58 | **Code: `ERR_NON_2XX_3XX_RESPONSE`** 59 | 60 | When the request is unsuccessful. 61 | 62 | A request is successful when the status code of the final request is `2xx` or `3xx`. 63 | 64 | When [following redirects](2-options.md#followredirect), a request is successful **only** when the status code of the final request is `2xx`. 65 | 66 | > [!NOTE] 67 | > `304` responses are always considered successful. 68 | 69 | ### `MaxRedirectsError` 70 | 71 | **Code: `ERR_TOO_MANY_REDIRECTS`** 72 | 73 | When the server redirects you more than ten times. Includes a `response` property. 74 | 75 | ### `UnsupportedProtocolError` 76 | 77 | > [!NOTE] 78 | > This error is not public. 79 | 80 | **Code: `ERR_UNSUPPORTED_PROTOCOL`** 81 | 82 | When given an unsupported protocol. 83 | 84 | ### `TimeoutError` 85 | 86 | **Code: `ETIMEDOUT`** 87 | 88 | When the request is aborted due to a [timeout](6-timeout.md). Includes an `event` (a string) property along with `timings`. 89 | 90 | ### `CancelError` 91 | 92 | **Code: `ERR_CANCELED`** 93 | 94 | When the request is aborted with `promise.cancel()`. 95 | 96 | ### `RetryError` 97 | 98 | **Code: `ERR_RETRYING`** 99 | 100 | Always triggers a new retry when thrown. 101 | 102 | ### `AbortError` 103 | 104 | **Code: `ERR_ABORTED`** 105 | 106 | When the request is aborted with [AbortController.abort()](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort). 107 | -------------------------------------------------------------------------------- /documentation/9-hooks.md: -------------------------------------------------------------------------------- 1 | [> Back to homepage](../readme.md#documentation) 2 | 3 | ## Hooks API 4 | 5 | ### `hooks` 6 | 7 | **Type: `object`** 8 | 9 | This option represents the hooks to run. Thrown errors will be automatically converted to [`RequestError`](8-errors.md#requesterror). 10 | 11 | #### `init` 12 | 13 | **Type: `InitHook[]`**\ 14 | **Default: `[]`** 15 | 16 | ```ts 17 | (plainRequestOptions: OptionsInit, options: Options) => void 18 | ``` 19 | 20 | Called with the plain request options, right before their normalization.\ 21 | The second argument represents the current [`Options`](2-options.md) instance. 22 | 23 | **Note:** 24 | > - This hook must be synchronous. 25 | 26 | **Note:** 27 | > - This is called every time options are merged. 28 | 29 | **Note:** 30 | > - The `options` object may not have the `url` property. To modify it, use a `beforeRequest` hook instead. 31 | 32 | **Note:** 33 | > - This hook is called when a new instance of `Options` is created. 34 | > - Do not confuse this with the creation of `Request` or `got(…)`. 35 | 36 | **Note:** 37 | > - When using `got(url)` or `got(url, undefined, defaults)` this hook will **not** be called. 38 | 39 | This is especially useful in conjunction with `got.extend()` when the input needs custom handling. 40 | 41 | For example, this can be used to fix typos to migrate from older versions faster. 42 | 43 | ```js 44 | import got from 'got'; 45 | 46 | const instance = got.extend({ 47 | hooks: { 48 | init: [ 49 | plain => { 50 | if ('followRedirects' in plain) { 51 | plain.followRedirect = plain.followRedirects; 52 | delete plain.followRedirects; 53 | } 54 | } 55 | ] 56 | } 57 | }); 58 | 59 | // Normally, the following would throw: 60 | const response = await instance( 61 | 'https://example.com', 62 | { 63 | followRedirects: true 64 | } 65 | ); 66 | 67 | // There is no option named `followRedirects`, but we correct it in an `init` hook. 68 | ``` 69 | 70 | Or you can create your own option and store it in a context: 71 | 72 | ```js 73 | import got from 'got'; 74 | 75 | const instance = got.extend({ 76 | hooks: { 77 | init: [ 78 | (plain, options) => { 79 | if ('secret' in plain) { 80 | options.context.secret = plain.secret; 81 | delete plain.secret; 82 | } 83 | } 84 | ], 85 | beforeRequest: [ 86 | options => { 87 | options.headers.secret = options.context.secret; 88 | } 89 | ] 90 | } 91 | }); 92 | 93 | const {headers} = await instance( 94 | 'https://httpbin.org/anything', 95 | { 96 | secret: 'passphrase' 97 | } 98 | ).json(); 99 | 100 | console.log(headers.Secret); 101 | //=> 'passphrase' 102 | ``` 103 | 104 | #### `beforeRequest` 105 | 106 | **Type: `BeforeRequestHook[]`**\ 107 | **Default: `[]`** 108 | 109 | ```ts 110 | (options: Options) => Promisable 111 | ``` 112 | 113 | Called right before making the request with `options.createNativeRequestOptions()`.\ 114 | This hook is especially useful in conjunction with `got.extend()` when you want to sign your request. 115 | 116 | **Note:** 117 | > - Got will make no further changes to the request before it is sent. 118 | 119 | **Note:** 120 | > - Changing `options.json` or `options.form` has no effect on the request. You should change `options.body` instead. If needed, update the `options.headers` accordingly. 121 | 122 | ```js 123 | import got from 'got'; 124 | 125 | const response = await got.post( 126 | 'https://httpbin.org/anything', 127 | { 128 | json: {payload: 'old'}, 129 | hooks: { 130 | beforeRequest: [ 131 | options => { 132 | options.body = JSON.stringify({payload: 'new'}); 133 | options.headers['content-length'] = options.body.length.toString(); 134 | } 135 | ] 136 | } 137 | } 138 | ); 139 | ``` 140 | 141 | **Tip:** 142 | > - You can indirectly override the `request` function by early returning a [`ClientRequest`-like](https://nodejs.org/api/http.html#http_class_http_clientrequest) instance or a [`IncomingMessage`-like](https://nodejs.org/api/http.html#http_class_http_incomingmessage) instance. This is very useful when creating a custom cache mechanism. 143 | > - [Read more about this tip](cache.md#advanced-caching-mechanisms). 144 | 145 | #### `beforeRedirect` 146 | 147 | **Type: `BeforeRedirectHook[]`**\ 148 | **Default: `[]`** 149 | 150 | ```ts 151 | (updatedOptions: Options, plainResponse: PlainResponse) => Promisable 152 | ``` 153 | 154 | The equivalent of `beforeRequest` but when redirecting. 155 | 156 | **Tip:** 157 | > - This is especially useful when you want to avoid dead sites. 158 | 159 | ```js 160 | import got from 'got'; 161 | 162 | const response = await got('https://example.com', { 163 | hooks: { 164 | beforeRedirect: [ 165 | (options, response) => { 166 | if (options.hostname === 'deadSite') { 167 | options.hostname = 'fallbackSite'; 168 | } 169 | } 170 | ] 171 | } 172 | }); 173 | ``` 174 | 175 | #### `beforeRetry` 176 | 177 | **Type: `BeforeRetryHook[]`**\ 178 | **Default: `[]`** 179 | 180 | ```ts 181 | (error: RequestError, retryCount: number) => Promisable 182 | ``` 183 | 184 | The equivalent of `beforeError` but when retrying. Additionally, there is a second argument `retryCount`, the current retry number. 185 | 186 | **Note:** 187 | > - When using the Stream API, this hook is ignored. 188 | 189 | **Note:** 190 | > - When retrying, the `beforeRequest` hook is called afterwards. 191 | 192 | **Note:** 193 | > - If no retry occurs, the `beforeError` hook is called instead. 194 | 195 | This hook is especially useful when you want to retrieve the cause of a retry. 196 | 197 | ```js 198 | import got from 'got'; 199 | 200 | await got('https://httpbin.org/status/500', { 201 | hooks: { 202 | beforeRetry: [ 203 | (error, retryCount) => { 204 | console.log(`Retrying [${retryCount}]: ${error.code}`); 205 | // Retrying [1]: ERR_NON_2XX_3XX_RESPONSE 206 | } 207 | ] 208 | } 209 | }); 210 | ``` 211 | 212 | #### `afterResponse` 213 | 214 | **Type: `AfterResponseHook[]`**\ 215 | **Default: `[]`** 216 | 217 | ```ts 218 | (response: Response, retryWithMergedOptions: (options: OptionsInit) => never) => Promisable> 219 | ``` 220 | 221 | Each function should return the response. This is especially useful when you want to refresh an access token. 222 | 223 | **Note:** 224 | > - When using the Stream API, this hook is ignored. 225 | 226 | **Note:** 227 | > - Calling the `retryWithMergedOptions` function will trigger `beforeRetry` hooks. If the retry is successful, all remaining `afterResponse` hooks will be called. In case of an error, `beforeRetry` hooks will be called instead. 228 | Meanwhile the `init`, `beforeRequest` , `beforeRedirect` as well as already executed `afterResponse` hooks will be skipped. 229 | 230 | ```js 231 | import got from 'got'; 232 | 233 | const instance = got.extend({ 234 | hooks: { 235 | afterResponse: [ 236 | (response, retryWithMergedOptions) => { 237 | // Unauthorized 238 | if (response.statusCode === 401) { 239 | // Refresh the access token 240 | const updatedOptions = { 241 | headers: { 242 | token: getNewToken() 243 | } 244 | }; 245 | 246 | // Update the defaults 247 | instance.defaults.options.merge(updatedOptions); 248 | 249 | // Make a new retry 250 | return retryWithMergedOptions(updatedOptions); 251 | } 252 | 253 | // No changes otherwise 254 | return response; 255 | } 256 | ], 257 | beforeRetry: [ 258 | error => { 259 | // This will be called on `retryWithMergedOptions(...)` 260 | } 261 | ] 262 | }, 263 | mutableDefaults: true 264 | }); 265 | ``` 266 | 267 | #### `beforeError` 268 | 269 | **Type: `BeforeErrorHook[]`**\ 270 | **Default: `[]`** 271 | 272 | ```ts 273 | (error: RequestError) => Promisable 274 | ``` 275 | 276 | Called with a [`RequestError`](8-errors.md#requesterror) instance. The error is passed to the hook right before it's thrown. 277 | 278 | This is especially useful when you want to have more detailed errors. 279 | 280 | ```js 281 | import got from 'got'; 282 | 283 | await got('https://api.github.com/repos/sindresorhus/got/commits', { 284 | responseType: 'json', 285 | hooks: { 286 | beforeError: [ 287 | error => { 288 | const {response} = error; 289 | if (response && response.body) { 290 | error.name = 'GitHubError'; 291 | error.message = `${response.body.message} (${response.statusCode})`; 292 | } 293 | 294 | return error; 295 | } 296 | ] 297 | } 298 | }); 299 | ``` 300 | -------------------------------------------------------------------------------- /documentation/cache.md: -------------------------------------------------------------------------------- 1 | [> Back to homepage](../readme.md#documentation) 2 | 3 | ## Cache 4 | 5 | Got implements [RFC 7234](https://httpwg.org/specs/rfc7234.html) compliant HTTP caching which works out of the box in-memory and is easily pluggable with a wide range of storage adapters. Fresh cache entries are served directly from the cache, and stale cache entries are revalidated with `If-None-Match` / `If-Modified-Since` headers. You can read more about the underlying cache behavior in the [`cacheable-request` documentation](https://www.npmjs.com/package/cacheable-request). 6 | 7 | You can use the JavaScript `Map` type as an in-memory cache: 8 | 9 | ```js 10 | import got from 'got'; 11 | 12 | const map = new Map(); 13 | 14 | let response = await got('https://sindresorhus.com', {cache: map}); 15 | console.log(response.isFromCache); 16 | //=> false 17 | 18 | response = await got('https://sindresorhus.com', {cache: map}); 19 | console.log(response.isFromCache); 20 | //=> true 21 | ``` 22 | 23 | Got uses [Keyv](https://github.com/lukechilds/keyv) internally to support a wide range of storage adapters. For something more scalable you could use an [official Keyv storage adapter](https://github.com/lukechilds/keyv#official-storage-adapters): 24 | 25 | ``` 26 | $ npm install @keyv/redis 27 | ``` 28 | 29 | ```js 30 | import got from 'got'; 31 | import KeyvRedis from '@keyv/redis'; 32 | 33 | const redis = new KeyvRedis('redis://user:pass@localhost:6379'); 34 | 35 | await got('https://sindresorhus.com', {cache: redis}); 36 | ``` 37 | 38 | Got supports anything that follows the Map API, so it's easy to write your own storage adapter or use a third-party solution. 39 | 40 | For example, the following are all valid storage adapters: 41 | 42 | ```js 43 | const storageAdapter = new Map(); 44 | 45 | await got('https://sindresorhus.com', {cache: storageAdapter}); 46 | ``` 47 | 48 | ```js 49 | import storageAdapter from './my-storage-adapter'; 50 | 51 | await got('https://sindresorhus.com', {cache: storageAdapter}); 52 | ``` 53 | 54 | ```js 55 | import QuickLRU from 'quick-lru'; 56 | 57 | const storageAdapter = new QuickLRU({maxSize: 1000}); 58 | 59 | await got('https://sindresorhus.com', {cache: storageAdapter}); 60 | ``` 61 | 62 | View the [Keyv docs](https://github.com/lukechilds/keyv) for more information on how to use storage adapters. 63 | 64 | ### Advanced caching mechanisms 65 | 66 | The `request` function may return an instance of `IncomingMessage`-like class. 67 | 68 | ```js 69 | import https from 'node:https'; 70 | import {Readable} from 'node:stream'; 71 | import got from 'got'; 72 | 73 | const getCachedResponse = (url, options) => { 74 | const response = new Readable({ 75 | read() { 76 | this.push("Hello, world!"); 77 | this.push(null); 78 | } 79 | }); 80 | 81 | response.statusCode = 200; 82 | response.headers = {}; 83 | response.trailers = {}; 84 | response.socket = null; 85 | response.aborted = false; 86 | response.complete = true; 87 | response.httpVersion = '1.1'; 88 | response.httpVersionMinor = 1; 89 | response.httpVersionMajor = 1; 90 | 91 | return response; 92 | }; 93 | 94 | const instance = got.extend({ 95 | request: (url, options, callback) => { 96 | return getCachedResponse(url, options); 97 | } 98 | }); 99 | 100 | const body = await instance('https://example.com').text(); 101 | 102 | console.log(body); 103 | //=> "Hello, world!" 104 | ``` 105 | 106 | If you don't want to alter the `request` function, you can return a cached response in a `beforeRequest` hook: 107 | 108 | ```js 109 | import https from 'node:https'; 110 | import {Readable} from 'node:stream'; 111 | import got from 'got'; 112 | 113 | const getCachedResponse = (url, options) => { 114 | const response = new Readable({ 115 | read() { 116 | this.push("Hello, world!"); 117 | this.push(null); 118 | } 119 | }); 120 | 121 | response.statusCode = 200; 122 | response.headers = {}; 123 | response.trailers = {}; 124 | response.socket = null; 125 | response.aborted = false; 126 | response.complete = true; 127 | response.httpVersion = '1.1'; 128 | response.httpVersionMinor = 1; 129 | response.httpVersionMajor = 1; 130 | 131 | return response; 132 | }; 133 | 134 | const instance = got.extend({ 135 | hooks: { 136 | beforeRequest: [ 137 | options => { 138 | return getCachedResponse(options.url, options); 139 | } 140 | ] 141 | } 142 | }); 143 | 144 | const body = await instance('https://example.com').text(); 145 | 146 | console.log(body); 147 | //=> "Hello, world!" 148 | ``` 149 | 150 | If you want to prevent duplicating the same requests, you can use a handler instead. 151 | 152 | ```js 153 | import got from 'got'; 154 | 155 | const map = new Map(); 156 | 157 | const instance = got.extend({ 158 | handlers: [ 159 | (options, next) => { 160 | if (options.isStream) { 161 | return next(options); 162 | } 163 | 164 | const pending = map.get(options.url.href); 165 | if (pending) { 166 | return pending; 167 | } 168 | 169 | const promise = next(options); 170 | 171 | map.set(options.url.href, promise); 172 | promise.finally(() => { 173 | map.delete(options.url.href); 174 | }); 175 | 176 | return promise; 177 | } 178 | ] 179 | }); 180 | 181 | const [first, second] = await Promise.all([ 182 | instance('https://httpbin.org/anything'), 183 | instance('https://httpbin.org/anything') 184 | ]); 185 | 186 | console.log(first === second); 187 | //=> true 188 | ``` 189 | -------------------------------------------------------------------------------- /documentation/examples/advanced-creation.js: -------------------------------------------------------------------------------- 1 | import got from '../../dist/source/index.js'; 2 | 3 | /* 4 | * Got supports composing multiple instances together. This is very powerful. 5 | * 6 | * You can create a client that limits download speed, 7 | * then compose it with an instance that signs a request. 8 | * 9 | * It's like plugins without any of the plugin mess. 10 | * You just create instances and then compose them together. 11 | * 12 | * To mix them use `instanceA.extend(instanceB, instanceC, ...)`, that's all. 13 | * Let's begin. 14 | */ 15 | 16 | // Logging all `got(…)` calls 17 | const logger = got.extend({ 18 | handlers: [ 19 | (options, next) => { 20 | console.log(`Sending ${options.method} to ${options.url}`); 21 | return next(options); 22 | } 23 | ] 24 | }); 25 | 26 | // Denying redirects to foreign hosts 27 | const controlRedirects = got.extend({ 28 | hooks: { 29 | beforeRedirect: [ 30 | (options, response) => { 31 | const {origin} = response.request.options.url; 32 | if (options.url.origin !== origin) { 33 | throw new Error(`Redirection to ${options.url.origin} is not allowed from ${origin}`); 34 | } 35 | } 36 | ] 37 | } 38 | }); 39 | 40 | // Limiting download & upload size 41 | // This can prevent crashing due to insufficient memory 42 | const limitDownloadUpload = got.extend({ 43 | handlers: [ 44 | (options, next) => { 45 | const {downloadLimit, uploadLimit} = options.context; 46 | let promiseOrStream = next(options); 47 | 48 | // A destroy function that supports both promises and streams 49 | const destroy = message => { 50 | if (options.isStream) { 51 | promiseOrStream.destroy(new Error(message)); 52 | return; 53 | } 54 | 55 | promiseOrStream.cancel(message); 56 | }; 57 | 58 | if (typeof downloadLimit === 'number') { 59 | promiseOrStream.on('downloadProgress', progress => { 60 | if (progress.transferred > downloadLimit && progress.percent !== 1) { 61 | destroy(`Exceeded the download limit of ${downloadLimit} bytes`); 62 | } 63 | }); 64 | } 65 | 66 | if (typeof uploadLimit === 'number') { 67 | promiseOrStream.on('uploadProgress', progress => { 68 | if (progress.transferred > uploadLimit && progress.percent !== 1) { 69 | destroy(`Exceeded the upload limit of ${uploadLimit} bytes`); 70 | } 71 | }); 72 | } 73 | 74 | return promiseOrStream; 75 | } 76 | ] 77 | }); 78 | 79 | // No user agent 80 | const noUserAgent = got.extend({ 81 | headers: { 82 | 'user-agent': undefined 83 | } 84 | }); 85 | 86 | // Custom endpoint 87 | const httpbin = got.extend({ 88 | prefixUrl: 'https://httpbin.org/' 89 | }); 90 | 91 | // Signing requests 92 | import crypto from 'node:crypto'; 93 | 94 | const getMessageSignature = (data, secret) => crypto.createHmac('sha256', secret).update(data).digest('hex').toUpperCase(); 95 | const signRequest = got.extend({ 96 | hooks: { 97 | beforeRequest: [ 98 | options => { 99 | const secret = options.context.secret ?? process.env.SECRET; 100 | 101 | if (secret) { 102 | options.headers['sign'] = getMessageSignature(options.body ?? '', secret); 103 | } 104 | } 105 | ] 106 | } 107 | }); 108 | 109 | /* 110 | * Putting it all together 111 | */ 112 | const merged = got.extend( 113 | noUserAgent, 114 | logger, 115 | limitDownloadUpload, 116 | httpbin, 117 | signRequest, 118 | controlRedirects 119 | ); 120 | 121 | // There's no 'user-agent' header :) 122 | const {headers} = await merged.post('anything', { 123 | body: 'foobar', 124 | context: { 125 | secret: 'password' 126 | } 127 | }).json(); 128 | 129 | console.log(headers); 130 | // Sending POST to https://httpbin.org/anything 131 | // { 132 | // Accept: 'application/json', 133 | // 'Accept-Encoding': 'gzip, deflate, br', 134 | // 'Content-Length': '6', 135 | // Host: 'httpbin.org', 136 | // Sign: 'EB0167A1EBF205510BAFF5DA1465537944225F0E0140E1880B746F361FF11DCA' 137 | // } 138 | 139 | const MEGABYTE = 1048576; 140 | await merged('https://pop-iso.sfo2.cdn.digitaloceanspaces.com/21.04/amd64/intel/5/pop-os_21.04_amd64_intel_5.iso', { 141 | context: { 142 | downloadLimit: MEGABYTE 143 | }, 144 | prefixUrl: '' 145 | }); 146 | // CancelError: Exceeded the download limit of 1048576 bytes 147 | -------------------------------------------------------------------------------- /documentation/examples/gh-got.js: -------------------------------------------------------------------------------- 1 | import got from '../../dist/source/index.js'; 2 | 3 | const packageJson = { 4 | name: 'gh-got', 5 | version: '12.0.0' 6 | }; 7 | 8 | const getRateLimit = headers => ({ 9 | limit: Number.parseInt(headers['x-ratelimit-limit'], 10), 10 | remaining: Number.parseInt(headers['x-ratelimit-remaining'], 10), 11 | reset: new Date(Number.parseInt(headers['x-ratelimit-reset'], 10) * 1000) 12 | }); 13 | 14 | const instance = got.extend({ 15 | prefixUrl: 'https://api.github.com', 16 | headers: { 17 | accept: 'application/vnd.github.v3+json', 18 | 'user-agent': `${packageJson.name}/${packageJson.version}` 19 | }, 20 | responseType: 'json', 21 | context: { 22 | token: process.env.GITHUB_TOKEN, 23 | }, 24 | hooks: { 25 | init: [ 26 | (raw, options) => { 27 | if ('token' in raw) { 28 | options.context.token = raw.token; 29 | delete raw.token; 30 | } 31 | } 32 | ] 33 | }, 34 | handlers: [ 35 | (options, next) => { 36 | // Authorization 37 | const {token} = options.context; 38 | if (token && !options.headers.authorization) { 39 | options.headers.authorization = `token ${token}`; 40 | } 41 | 42 | // Don't touch streams 43 | if (options.isStream) { 44 | return next(options); 45 | } 46 | 47 | // Magic begins 48 | return (async () => { 49 | try { 50 | const response = await next(options); 51 | 52 | // Rate limit for the Response object 53 | response.rateLimit = getRateLimit(response.headers); 54 | 55 | return response; 56 | } catch (error) { 57 | const {response} = error; 58 | 59 | // Nicer errors 60 | if (response && response.body) { 61 | error.name = 'GitHubError'; 62 | error.message = `${response.body.message} (${response.statusCode} status code)`; 63 | } 64 | 65 | // Rate limit for errors 66 | if (response) { 67 | error.rateLimit = getRateLimit(response.headers); 68 | } 69 | 70 | throw error; 71 | } 72 | })(); 73 | } 74 | ] 75 | }); 76 | 77 | export default instance; 78 | -------------------------------------------------------------------------------- /documentation/examples/h2c.js: -------------------------------------------------------------------------------- 1 | import http2 from 'http2-wrapper'; 2 | import got from '../../dist/source/index.js'; 3 | 4 | let sessions = {}; 5 | const getSession = ({origin}) => { 6 | if (sessions[origin] && !sessions[origin].destroyed) { 7 | return sessions[origin]; 8 | } 9 | 10 | const session = http2.connect(origin); 11 | session.once('error', () => { 12 | delete sessions[origin]; 13 | }); 14 | 15 | sessions[origin] = session; 16 | 17 | return session; 18 | }; 19 | 20 | const closeSessions = () => { 21 | for (const key in sessions) { 22 | sessions[key].close(); 23 | } 24 | 25 | sessions = {}; 26 | }; 27 | 28 | const instance = got.extend({ 29 | hooks: { 30 | beforeRequest: [ 31 | options => { 32 | options.h2session = getSession(options.url); 33 | options.http2 = true; 34 | options.request = http2.request; 35 | } 36 | ] 37 | } 38 | }); 39 | 40 | const server = http2.createServer((request, response) => { 41 | response.end('{}'); 42 | }); 43 | 44 | server.listen(async () => { 45 | const url = `http://localhost:${server.address().port}`; 46 | const {body, headers} = await instance(url, {context: {h2c: true}}); 47 | console.log(headers, body); 48 | 49 | closeSessions(); 50 | server.close(); 51 | }); 52 | -------------------------------------------------------------------------------- /documentation/examples/pagination.js: -------------------------------------------------------------------------------- 1 | import got from '../../dist/source/index.js'; 2 | import Bourne from '@hapi/bourne'; 3 | 4 | const max = Date.now() - 1000 * 86400 * 7; 5 | 6 | const iterator = got.paginate('https://api.github.com/repos/sindresorhus/got/commits', { 7 | pagination: { 8 | paginate: ({response, currentItems}) => { 9 | // If there are no more data, finish. 10 | if (currentItems.length === 0) { 11 | return false; 12 | } 13 | 14 | // Get the current page number. 15 | const {searchParams} = response.request.options; 16 | const previousPage = Number(searchParams.get('page') ?? 1); 17 | 18 | // Update the page number by one. 19 | return { 20 | searchParams: { 21 | page: previousPage + 1 22 | } 23 | }; 24 | }, 25 | // Using `Bourne` to prevent prototype pollution. 26 | transform: response => Bourne.parse(response.body), 27 | filter: ({item}) => { 28 | // Check if the commit time exceeds our range. 29 | const date = new Date(item.commit.committer.date); 30 | const end = date.getTime() - max >= 0; 31 | 32 | return end; 33 | }, 34 | shouldContinue: ({item}) => { 35 | // Check if the commit time exceeds our range. 36 | const date = new Date(item.commit.committer.date); 37 | const end = date.getTime() - max >= 0; 38 | 39 | return end; 40 | }, 41 | // We want only 50 results. 42 | countLimit: 50, 43 | // Wait 1s before making another request to prevent API rate limiting. 44 | backoff: 1000, 45 | // It is a good practice to set an upper limit of how many requests can be made. 46 | // This way we can avoid infinite loops. 47 | requestLimit: 10, 48 | // In this case, we don't need to store all the items we receive. 49 | // They are processed immediately. 50 | stackAllItems: false 51 | } 52 | }); 53 | 54 | console.log('Last 50 commits from now to week ago:'); 55 | for await (const item of iterator) { 56 | console.log(item.commit.message.split('\n')[0]); 57 | } 58 | -------------------------------------------------------------------------------- /documentation/examples/runkit-example.js: -------------------------------------------------------------------------------- 1 | import got from 'got'; 2 | 3 | const issUrl = 'http://api.open-notify.org/iss-now.json'; 4 | 5 | const {iss_position: issPosition} = await got(issUrl).json(); 6 | 7 | console.log(issPosition); 8 | //=> {latitude: '20.4956', longitude: '42.2216'} 9 | -------------------------------------------------------------------------------- /documentation/examples/uppercase-headers.js: -------------------------------------------------------------------------------- 1 | import http from 'node:http'; 2 | import got from '../../dist/source/index.js'; 3 | 4 | // Wraps an existing Agent instance 5 | class WrappedAgent { 6 | constructor(agent) { 7 | this.agent = agent; 8 | } 9 | 10 | addRequest(request, options) { 11 | return this.agent.addRequest(request, options); 12 | } 13 | 14 | get keepAlive() { 15 | return this.agent.keepAlive; 16 | } 17 | 18 | get maxSockets() { 19 | return this.agent.maxSockets; 20 | } 21 | 22 | get options() { 23 | return this.agent.options; 24 | } 25 | 26 | get defaultPort() { 27 | return this.agent.defaultPort; 28 | } 29 | 30 | get protocol() { 31 | return this.agent.protocol; 32 | } 33 | } 34 | 35 | class TransformHeadersAgent extends WrappedAgent { 36 | addRequest(request, options) { 37 | const headers = request.getHeaderNames(); 38 | 39 | for (const header of headers) { 40 | request.setHeader(this.transformHeader(header), request.getHeader(header)); 41 | } 42 | 43 | return super.addRequest(request, options); 44 | } 45 | 46 | transformHeader(header) { 47 | return header.split('-').map(part => { 48 | return part[0].toUpperCase() + part.slice(1); 49 | }).join('-'); 50 | } 51 | } 52 | 53 | const agent = new http.Agent({ 54 | keepAlive: true 55 | }); 56 | 57 | const wrappedAgent = new TransformHeadersAgent(agent); 58 | 59 | const main = async () => { 60 | const headers = await got(`http://localhost:${server.address().port}`, { 61 | agent: { 62 | http: wrappedAgent 63 | }, 64 | headers: { 65 | foo: 'bar' 66 | } 67 | }).json(); 68 | 69 | console.log(headers); 70 | 71 | agent.destroy(); 72 | server.close(); 73 | }; 74 | 75 | const server = http.createServer((request, response) => { 76 | const {rawHeaders} = request; 77 | const headers = {}; 78 | 79 | for (let i = 0; i < rawHeaders.length; i += 2) { 80 | headers[rawHeaders[i]] = rawHeaders[i + 1]; 81 | } 82 | 83 | response.end(JSON.stringify(headers)); 84 | }).listen(0, main); 85 | -------------------------------------------------------------------------------- /documentation/migration-guides/axios.md: -------------------------------------------------------------------------------- 1 | [> Back to homepage](../../readme.md#documentation) 2 | 3 | ## Migration guides 4 | 5 | > You may think it's too hard to switch, but it's really not. 🦄 6 | 7 | ### Axios 8 | 9 | Axios is very similar to Got. The difference is that Axios targets browsers first, while Got fully makes use of Node.js features. 10 | 11 | #### Common options 12 | 13 | These options remain the same as well: 14 | 15 | - [`url`](../2-options.md#url) 16 | - [`method`](../2-options.md#method) 17 | - [`headers`](../2-options.md#headers) 18 | - [`maxRedirects`](../2-options.md#maxredirects) 19 | - [`decompress`](../2-options.md#decompress) 20 | 21 | #### Renamed options 22 | 23 | We deeply care about readability, so we renamed these options: 24 | 25 | - `httpAgent` → [`agent.http`](../2-options.md#agent) 26 | - `httpsAgent` → [`agent.https`](../2-options.md#agent) 27 | - `socketPath` → [`url`](../2-options.md#enableunixsockets) 28 | - `responseEncoding` → [`encoding`](../2-options.md#encoding) 29 | - `auth.username` → [`username`](../2-options.md#username) 30 | - `auth.password` → [`password`](../2-options.md#password) 31 | - `data` → [`body`](../2-options.md#body) / [`json`](../2-options.md#json) / [`form`](../2-options.md#form) 32 | - `params` → [`searchParams`](../2-options.md#serachparams) 33 | 34 | #### Changes in behavior 35 | 36 | - `transformRequest` → [`hooks.beforeRequest`](../9-hooks.md#beforerequest) 37 | - The API is different. 38 | - `transformResponse` → [`hooks.afterResponse`](../9-hooks.md#afterresponse) 39 | - The API is different. 40 | - `baseUrl` → [`prefixUrl`](../2-options.md#prefixurl) 41 | - The `prefixUrl` is always prepended to the `url`. 42 | - [`timeout`](../6-timeout.md) 43 | - This option is now an object. You can now set timeouts on particular events! 44 | - [`responseType`](../2-options.md#responsetype) 45 | - Accepts `'text'`, `'json'` or `'buffer'`. 46 | 47 | #### Breaking changes 48 | 49 | - `onUploadProgress` 50 | - This option does not exist. Instead, use [`got(…).on('uploadProgress', …)`](../3-streams.md#uploadprogress). 51 | - `onDownloadProgress` 52 | - This option does not exist. Instead, use [`got(…).on('downloadProgress', …)`](../3-streams.md#downloadprogress). 53 | - `maxContentLength` 54 | - This option does not exist. Instead, use [a handler](../examples/advanced-creation.js). 55 | - `validateStatus` 56 | - This option does not exist. Got automatically validates the status according to [the specification](https://datatracker.ietf.org/doc/html/rfc7231#section-6). 57 | - `proxy` 58 | - This option does not exist. You need to pass [an `agent`](../tips.md#proxy) instead. 59 | - `cancelToken` 60 | - This option does not exist, but will be implemented soon. For now, use `promise.cancel()` or `stream.destroy()`. 61 | - `paramsSerializer` 62 | - This option does not exist. 63 | - `maxBodyLength` 64 | - This option does not exist. 65 | 66 | #### Response 67 | 68 | The response object is different as well: 69 | 70 | - `response.data` → [`response.body`](../3-streams.md#response-1) 71 | - `response.status` → [`response.statusCode`](../3-streams.md#response-1) 72 | - `response.statusText` → [`response.statusMessage`](../3-streams.md#response-1) 73 | - `response.config` → [`response.request.options`](../3-streams.md#response-1) 74 | - [`response.request`](../3-streams.md#response-1) 75 | - Returns [a Got stream](../3-streams.md). 76 | 77 | The `response.headers` object remains the same. 78 | 79 | #### Interceptors 80 | 81 | Got offers [hooks](../9-hooks.md) instead, which are more flexible. 82 | 83 | #### Errors 84 | 85 | Errors look the same, with the difference `error.request` returns a Got stream. Furthermore, Got provides [more details](../8-errors.md) to make debugging easier. 86 | 87 | #### Cancelation 88 | 89 | While Got doesn't support [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) yet, you can use `promise.cancel()`. 90 | 91 | #### Convenience methods 92 | 93 | Convenience methods, such as `axios.get(…)` etc. remain the same: `got.get(…)`. Instead of `axios.create(…)` use `got.extend(…)`. 94 | 95 | #### You're good to go! 96 | 97 | Well, you have already come this far :tada:\ 98 | Take a look at the [documentation](../../readme.md#documentation). It's worth the time to read it.\ 99 | There are [some great tips](../tips.md). 100 | 101 | 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). 102 | -------------------------------------------------------------------------------- /documentation/migration-guides/nodejs.md: -------------------------------------------------------------------------------- 1 | [> Back to homepage](../../readme.md#documentation) 2 | 3 | ## Migration guides 4 | 5 | > You may think it's too hard to switch, but it's really not. 🦄 6 | 7 | ### Node.js 8 | 9 | Let's make a simple request. With Node.js, this is: 10 | 11 | ```js 12 | import http from 'node:http'; 13 | 14 | const request = http.request('https://httpbin.org/anything', response => { 15 | if (response.statusCode >= 400) { 16 | request.destroy(new Error()); 17 | return; 18 | } 19 | 20 | const chunks = []; 21 | 22 | response.on('data', chunk => { 23 | chunks.push(chunk); 24 | }); 25 | 26 | response.once('end', () => { 27 | const buffer = Buffer.concat(chunks); 28 | 29 | if (response.statusCode >= 400) { 30 | const error = new Error(`Unsuccessful response: ${response.statusCode}`); 31 | error.body = buffer.toString(); 32 | return; 33 | } 34 | 35 | const text = buffer.toString(); 36 | 37 | console.log(text); 38 | }); 39 | 40 | response.once('error', console.error); 41 | }); 42 | 43 | request.once('error', console.error); 44 | request.end(); 45 | ``` 46 | 47 | With Got, this becomes: 48 | 49 | ```js 50 | import got from 'got'; 51 | 52 | try { 53 | const {body} = await got('https://httpbin.org/anything'); 54 | console.log(body); 55 | } catch (error) { 56 | console.error(error); 57 | } 58 | ``` 59 | 60 | Much cleaner. But what about streams? 61 | 62 | ```js 63 | import http from 'node:http'; 64 | import fs from 'node:fs'; 65 | 66 | const source = fs.createReadStream('article.txt'); 67 | 68 | const request = http.request('https://httpbin.org/anything', { 69 | method: 'POST' 70 | }, response => { 71 | response.pipe(fs.createWriteStream('httpbin.txt')); 72 | }); 73 | 74 | source.pipe(request); 75 | ``` 76 | 77 | Well, it's easy as that: 78 | 79 | ```js 80 | import got from 'got'; 81 | import {pipeline as streamPipeline} from 'node:stream/promises'; 82 | import fs from 'node:fs'; 83 | 84 | await streamPipeline( 85 | fs.createReadStream('article.txt'), 86 | got.stream.post('https://httpbin.org/anything'), 87 | fs.createWriteStream('httpbin.txt') 88 | ); 89 | ``` 90 | 91 | The advantage is that Got also handles errors automatically, so you don't have to create custom listeners. 92 | 93 | Furthermore, Got supports redirects, compression, advanced timeouts, cache, pagination, cookies, hooks, and more! 94 | 95 | #### What next? 96 | 97 | Unfortunately Got options differ too much from the Node.js options. It's not possible to provide a brief summary.\ 98 | Don't worry, you will learn them fast - they are easy to understand! Every option has an example attached. 99 | 100 | Take a look at the [documentation](../../readme.md#documentation). It's worth the time to read it.\ 101 | There are [some great tips](../tips.md). 102 | 103 | 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). 104 | -------------------------------------------------------------------------------- /documentation/migration-guides/request.md: -------------------------------------------------------------------------------- 1 | [> Back to homepage](../../readme.md#documentation) 2 | 3 | ## Migration guides 4 | 5 | > You may think it's too hard to switch, but it's really not. 🦄 6 | 7 | ### Request 8 | 9 | Let's take the very first example from [Request's readme](https://github.com/request/request#super-simple-to-use): 10 | 11 | ```js 12 | import request from 'request'; 13 | 14 | request('https://google.com', (error, response, body) => { 15 | console.log('error:', error); 16 | console.log('statusCode:', response && response.statusCode); 17 | console.log('body:', body); 18 | }); 19 | ``` 20 | 21 | With Got, it is: 22 | 23 | ```js 24 | import got from 'got'; 25 | 26 | try { 27 | const response = await got('https://google.com'); 28 | console.log('statusCode:', response.statusCode); 29 | console.log('body:', response.body); 30 | } catch (error) { 31 | console.log('error:', error); 32 | } 33 | ``` 34 | 35 | Looks better now, huh? 😎 36 | 37 | #### Common options 38 | 39 | These Got options are the same as with Request: 40 | 41 | - [`url`](../2-options.md#url) 42 | - [`body`](../2-options.md#body) 43 | - [`followRedirect`](../2-options.md#followredirect) 44 | - [`encoding`](../2-options.md#encoding) 45 | - [`maxRedirects`](../2-options.md#maxredirects) 46 | - [`localAddress`](../2-options.md#localaddress) 47 | - [`headers`](../2-options.md#headers) 48 | - [`createConnection`](../2-options.md#createconnection) 49 | - [UNIX sockets](../2-options.md#enableunixsockets): `http://unix:SOCKET:PATH` 50 | 51 | The `time` option does not exist, assume [it's always true](../6-timeout.md). 52 | 53 | So if you're familiar with these, you're good to go. 54 | 55 | #### Renamed options 56 | 57 | **Note:** 58 | > - Got stores HTTPS options inside [`httpsOptions`](../2-options.md#httpsoptions). Some of them have been renamed. [Read more](../5-https.md). 59 | 60 | Readability is very important to us, so we have different names for these options: 61 | 62 | - `qs` → [`searchParams`](../2-options.md#serachparams) 63 | - `strictSSL` → [`rejectUnauthorized`](../2-options.md#rejectunauthorized) 64 | - `gzip` → [`decompress`](../2-options.md#decompress) 65 | - `jar` → [`cookieJar`](../2-options.md#cookiejar) (accepts [`tough-cookie`](https://github.com/salesforce/tough-cookie) jar) 66 | - `jsonReviver` → [`parseJson`](../2-options.md#parsejson) 67 | - `jsonReplacer` → [`stringifyJson`](../2-options.md#stringifyjson) 68 | 69 | #### Changes in behavior 70 | 71 | - The [`agent` option](../2-options.md#agent) is now an object with `http`, `https` and `http2` properties. 72 | - The [`timeout` option](../6-timeout.md) is now an object. You can set timeouts on particular events! 73 | - The [`searchParams` option](https://github.com/sindresorhus/got#searchParams) is always serialized using [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams). 74 | - In order to pass a custom query string, provide it with the `url` option.\ 75 | `got('https://example.com', {searchParams: {test: ''}})` → `https://example.com/?test=`\ 76 | `got('https://example.com/?test')` → `https://example.com/?test` 77 | - To use streams, call `got.stream(url, options)` or `got(url, {…, isStream: true})`. 78 | 79 | #### Breaking changes 80 | 81 | - The `json` option is not a `boolean`, it's an `object`. It will be stringified and used as a body. 82 | - The `form` option is an `object` and will be used as `application/x-www-form-urlencoded` body. 83 | - All headers are converted to lowercase.\ 84 | According to [the spec](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2), the headers are case-insensitive. 85 | - No `oauth` / `hawk` / `aws` / `httpSignature` option.\ 86 | To sign requests, you need to create a [custom instance](../examples/advanced-creation.js). 87 | - No `agentClass` / `agentOptions` / `pool` option. 88 | - No `forever` option.\ 89 | You need to pass an agent with `keepAlive` option set to `true`. 90 | - No `proxy` option. You need to [pass a custom agent](../tips.md#proxy). 91 | - No `auth` option.\ 92 | You need to use [`username`](../2-options.md#username) / [`password`](../2-options.md#password) instead or set the `authorization` header manually. 93 | - No `baseUrl` option.\ 94 | Instead, there is [`prefixUrl`](../2-options.md#prefixurl) which appends a trailing slash if not present. 95 | - No `removeRefererHeader` option.\ 96 | You can remove the `referer` header in a [`beforeRequest` hook](../9-hooks.md#beforerequest). 97 | - No `followAllRedirects` option. 98 | 99 | Hooks are very powerful. [Read more](../9-hooks.md) to see what else you achieve using hooks. 100 | 101 | #### More about streams 102 | 103 | Let's take a quick look at another example from Request's readme: 104 | 105 | ```js 106 | http.createServer((serverRequest, serverResponse) => { 107 | if (serverRequest.url === '/doodle.png') { 108 | serverRequest.pipe(request('https://example.com/doodle.png')).pipe(serverResponse); 109 | } 110 | }); 111 | ``` 112 | 113 | The cool feature here is that Request can proxy headers with the stream, but Got can do that too! 114 | 115 | ```js 116 | import {pipeline as streamPipeline} from 'node:stream/promises'; 117 | import got from 'got'; 118 | 119 | const server = http.createServer(async (serverRequest, serverResponse) => { 120 | if (serverRequest.url === '/doodle.png') { 121 | await streamPipeline( 122 | got.stream('https://example.com/doodle.png'), 123 | serverResponse 124 | ); 125 | } 126 | }); 127 | 128 | server.listen(8080); 129 | ``` 130 | 131 | In terms of streams nothing has really changed. 132 | 133 | #### Convenience methods 134 | 135 | - If you were using `request.get`, `request.post`, and so on - you can do the same with Got. 136 | - The `request.defaults({…})` method has been renamed. You can do the same with `got.extend({…})`. 137 | - There is no `request.cookie()` nor `request.jar()`. You have to use `tough-cookie` directly. 138 | 139 | #### You're good to go! 140 | 141 | Well, you have already come this far :tada:\ 142 | Take a look at the [documentation](../../readme.md#documentation). It's worth the time to read it.\ 143 | There are [some great tips](../tips.md). 144 | 145 | 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). 146 | -------------------------------------------------------------------------------- /documentation/quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick Start Guide 2 | 3 | ## Getting and posting data with promises 4 | 5 | The simplest `GET` request: 6 | 7 | ```js 8 | import got from 'got'; 9 | 10 | const url = 'https://httpbin.org/anything'; 11 | const response = await got(url); 12 | ``` 13 | 14 | The call returns a Promise<[Response](3-streams.md#response-1)>. If the body contains JSON, it can be retrieved directly: 15 | 16 | ```js 17 | import got from 'got'; 18 | 19 | const url = 'https://httpbin.org/anything'; 20 | const data = await got(url).json(); 21 | ``` 22 | 23 | The similar [got.text](1-promise.md#promisetext) method returns plain text. 24 | 25 | All `got` methods accept an options object for passing extra configuration, such as headers: 26 | 27 | ```js 28 | import got from 'got'; 29 | 30 | const url = 'https://httpbin.org/anything'; 31 | 32 | const options = { 33 | headers: { 34 | 'Custom-Header': 'Quick start', 35 | }, 36 | timeout: { 37 | send: 3500 38 | }, 39 | }; 40 | 41 | const data = await got(url, options).json(); 42 | ``` 43 | 44 | A `POST` request is very similar: 45 | 46 | ```js 47 | import got from 'got'; 48 | 49 | const url = 'https://httpbin.org/anything'; 50 | 51 | const options = { 52 | json: { 53 | documentName: 'Quick Start', 54 | }, 55 | }; 56 | 57 | const data = await got.post(url, options); 58 | ``` 59 | 60 | The request body is passed in the options object. The `json` property will automatically set headers accordingly. Custom headers can be added exactly as above. 61 | 62 | ## Using streams 63 | 64 | The [Stream API](3-streams.md) allows to leverage [Node.js Streams](https://nodejs.dev/learn/nodejs-streams) capabilities: 65 | 66 | ```js 67 | import fs from 'node:fs'; 68 | import {pipeline as streamPipeline} from 'node:stream/promises'; 69 | import got from 'got'; 70 | 71 | const url = 'https://httpbin.org/anything'; 72 | 73 | const options = { 74 | json: { 75 | documentName: 'Quick Start', 76 | }, 77 | }; 78 | 79 | const gotStream = got.stream.post(url, options); 80 | 81 | const outStream = fs.createWriteStream('anything.json'); 82 | 83 | try { 84 | await streamPipeline(gotStream, outStream); 85 | } catch (error) { 86 | console.error(error); 87 | } 88 | ``` 89 | 90 | ## Options 91 | 92 | Options can be set at the client level and reused in subsequent queries: 93 | 94 | ```js 95 | import got from 'got'; 96 | 97 | const options = { 98 | prefixUrl: 'https://httpbin.org', 99 | headers: { 100 | Authorization: getTokenFromVault(), 101 | }, 102 | }; 103 | 104 | const client = got.extend(options); 105 | 106 | export default client; 107 | ``` 108 | 109 | Some noticeable common options are: 110 | - [`searchParams`](2-options.md#searchparams): A query string object. 111 | - [`prefixUrl`](2-options.md#prefixurl): Prepended to query paths. Paths must be relative to prefix, i.e. not begin with a `/`. 112 | - [`method`](2-options.md#method): The HTTP method name. 113 | - [`headers`](2-options.md#headers): Query headers. 114 | - [`json`](2-options.md#json): JSON body. 115 | - [`form`](2-options.md#form): A form query string object. 116 | 117 | See the documentation for other [options](2-options.md#options). 118 | 119 | ## Errors 120 | 121 | Both Promise and Stream APIs throw errors with metadata. 122 | 123 | ```js 124 | import got from 'got'; 125 | 126 | try { 127 | const data = await got.get('https://httpbin.org/status/404'); 128 | } catch (error) { 129 | console.error(error.response.statusCode); 130 | } 131 | ``` 132 | 133 | ```js 134 | import got from 'got'; 135 | 136 | const stream = got.stream 137 | .get('https://httpbin.org/status/404') 138 | .once('error', error => { 139 | console.error(error.response.statusCode); 140 | }); 141 | ``` 142 | 143 | ## Miscellaneous 144 | 145 | The HTTP method name can also be given as an option, this may be convenient when it is known only at runtime: 146 | 147 | ```js 148 | import got from 'got'; 149 | 150 | const url = 'https://httpbin.org/anything'; 151 | 152 | const method = 'POST'; 153 | 154 | const options = { 155 | method, 156 | json: { 157 | documentName: 'Quick Start', 158 | }, 159 | }; 160 | 161 | const data = await got(url, options); 162 | ``` 163 | 164 | For most apps, HTTP clients just do `GET` and `POST` queries (`PUT`, `PATCH` or `DELETE` methods work similarly). 165 | The following sections will give some pointers to more advanced usage. 166 | 167 | ### Timeouts 168 | 169 | By default, requests have no timeout. It is a good practice to set one: 170 | 171 | ```js 172 | import got from 'got'; 173 | 174 | const options = { 175 | timeout: { 176 | request: 10000, 177 | }, 178 | }; 179 | 180 | const client = got.extend(options); 181 | 182 | export default client; 183 | ``` 184 | 185 | The above sets a global timeout of 10000 milliseconds for all requests issued by the exported `client`. Like all options, timeouts can also be set at the request level. See the [`timeout` option](6-timeout.md#timeout-options). 186 | 187 | ### Retries 188 | 189 | A failed request is retried twice. The retry policy may be tuned with a [`retry`](7-retry.md#retry) options object. 190 | 191 | ```js 192 | import got from 'got'; 193 | 194 | const options = { 195 | retry: { 196 | limit: 5, 197 | errorCodes: [ 198 | 'ETIMEDOUT' 199 | ], 200 | }, 201 | }; 202 | ``` 203 | 204 | Retries with stream are a little trickier, see [`stream.on('retry', …)`](3-streams.md#streamonretry-). 205 | 206 | ### Hooks 207 | 208 | Hooks are custom functions called on some request events: 209 | 210 | ```js 211 | import got from 'got'; 212 | 213 | const logRetry = (error, retryCount) => { 214 | console.error(`Retrying after error ${error.code}, retry #: ${retryCount}`); 215 | }; 216 | 217 | const options = { 218 | hooks: { 219 | beforeRetry: [ 220 | logRetry, 221 | ], 222 | }, 223 | }; 224 | 225 | const client = got.extend(options); 226 | 227 | export default client; 228 | ``` 229 | 230 | *Note that hooks are given as arrays*, thus multiple hooks can be given. See documentation for other possible [hooks](9-hooks.md#hooks-api). 231 | 232 | ### Going further 233 | 234 | There is a lot more to discover in the [documentation](../readme.md#documentation) and [tips](tips.md#tips). Among others, `Got` can handle [cookies](tips.md#cookies), [pagination](4-pagination.md#pagination-api), [cache](cache.md#cache). Please read the documentation before implementing something that is already done by `Got` :innocent:. 235 | -------------------------------------------------------------------------------- /documentation/typescript.md: -------------------------------------------------------------------------------- 1 | [> Back to homepage](../readme.md#documentation) 2 | 3 | ## TypeScript 4 | 5 | Got is fully written in TypeScript, so the integration is seamless.\ 6 | Furthermore, types have saved Got from many bugs and inconsistencies. 7 | 8 | Here's a list of types that Got exports: 9 | 10 | **Note:** 11 | > - The list may be incomplete. If you find a type is missing, please open an issue about it. 12 | 13 | ### [`Got`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L200) 14 | 15 | ### [`GotStream`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L195) 16 | 17 | ### [`GotRequestFunction`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L142) 18 | 19 | ### [`GotPaginate`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L87) 20 | 21 | ### [`OptionsWithPagination`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L82) 22 | 23 | ### [`OptionsOfTextResponseBody`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L74) 24 | 25 | ### [`OptionsOfJSONResponseBody`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L75) 26 | 27 | ### [`OptionsOfBufferResponseBody`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L76) 28 | 29 | ### [`OptionsOfUnknownResponseBody`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L77) 30 | 31 | ### [`StrictOptions`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L78) 32 | 33 | ### [`StreamOptions`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L79) 34 | 35 | ### [`OptionsInit`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/core/options.ts#L535) 36 | 37 | ### [`ExtendOptions`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L55) 38 | 39 | ### [`PlainResponse`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/core/response.ts#L6) 40 | 41 | ### [`Response`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/core/response.ts#L95) 42 | 43 | ### [`Request`](https://github.com/sindresorhus/got/blob/ecb05343dea3bd35933585a1ec5bcea01348d109/source/core/index.ts#L139) 44 | 45 | ### [`RequestEvents`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/core/index.ts#L108) 46 | 47 | ### [`Progress`](https://github.com/sindresorhus/got/blob/0f9f2b83b77710f2dc08c2a6bce1c78ba8d46760/source/core/index.ts#L40) 48 | 49 | ### [`InstanceDefaults`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L17) 50 | 51 | ### [`GotReturn`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L44) 52 | 53 | ### [`HandlerFunction`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/types.ts#L50) 54 | 55 | ### [`CancelableRequest`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/as-promise/types.ts#L26) 56 | 57 | ### [`Delays`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/core/timed-out.ts#L14) 58 | 59 | ### [`CreateConnectionFunction`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/core/options.ts#L302) 60 | 61 | ### [`CheckServerIdentityFunction`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/core/options.ts#L303) 62 | 63 | ### [`CacheOptions`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/core/options.ts#L305) 64 | 65 | ### [`HttpsOptions`](https://github.com/sindresorhus/got/blob/ae04c0e36cf3f5b2e356df7d48a5b19988f935a2/source/core/options.ts#L319) 66 | 67 | ### [`PaginateData`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L382) 68 | 69 | ### [`PaginationOptions`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L397) 70 | 71 | ### [`SearchParameters`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L504) 72 | 73 | ### [`ResponseType`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L518) 74 | 75 | ### [`FilterData`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L388) 76 | 77 | ### [`RetryObject`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L259) 78 | 79 | ### [`RetryFunction`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L267) 80 | 81 | ### [`ParseJsonFunction`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L235) 82 | 83 | ### [`StringifyJsonFunction`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L236) 84 | 85 | ### [`Method`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L241) 86 | 87 | ### [`ToughCookieJar`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L53) 88 | 89 | ### [`PromiseCookieJar`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L60) 90 | 91 | ### [`DnsLookupIpVersion`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L35) 92 | 93 | ### [`RequestFunction`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L43) 94 | 95 | ### [`Agents`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L45) 96 | 97 | ### [`Headers`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L51) 98 | 99 | ### [`Hooks`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L75) 100 | 101 | ### [`InitHook`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L65) 102 | 103 | ### [`BeforeRequestHook`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L66) 104 | 105 | ### [`BeforeRedirectHook`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L67) 106 | 107 | ### [`BeforeErrorHook`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L68) 108 | 109 | ### [`BeforeRetryHook`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L69) 110 | 111 | ### [`AfterResponseHook`](https://github.com/sindresorhus/got/blob/215e06a4993329578d92d4f44607913239a03094/source/core/options.ts#L70) 112 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /media/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sindresorhus/got/a359bd385129d2adbc765b52dfbbadac5f54a825/media/logo.ai -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sindresorhus/got/a359bd385129d2adbc765b52dfbbadac5f54a825/media/logo.png -------------------------------------------------------------------------------- /media/logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sindresorhus/got/a359bd385129d2adbc765b52dfbbadac5f54a825/media/logo.sketch -------------------------------------------------------------------------------- /media/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "got", 3 | "version": "14.4.7", 4 | "description": "Human-friendly and powerful HTTP request library for Node.js", 5 | "license": "MIT", 6 | "repository": "sindresorhus/got", 7 | "funding": "https://github.com/sindresorhus/got?sponsor=1", 8 | "type": "module", 9 | "exports": { 10 | "types": "./dist/source/index.d.ts", 11 | "default": "./dist/source/index.js" 12 | }, 13 | "sideEffects": false, 14 | "engines": { 15 | "node": ">=20" 16 | }, 17 | "scripts": { 18 | "test": "xo && tsc --noEmit && NODE_OPTIONS='--import=tsx/esm' ava", 19 | "release": "np", 20 | "build": "del-cli dist && tsc", 21 | "prepare": "npm run build" 22 | }, 23 | "files": [ 24 | "dist/source" 25 | ], 26 | "keywords": [ 27 | "http", 28 | "https", 29 | "http2", 30 | "get", 31 | "got", 32 | "url", 33 | "uri", 34 | "request", 35 | "simple", 36 | "curl", 37 | "wget", 38 | "fetch", 39 | "net", 40 | "network", 41 | "gzip", 42 | "brotli", 43 | "requests", 44 | "human-friendly", 45 | "axios", 46 | "superagent", 47 | "node-fetch", 48 | "ky" 49 | ], 50 | "dependencies": { 51 | "@sindresorhus/is": "^7.0.1", 52 | "@szmarczak/http-timer": "^5.0.1", 53 | "cacheable-lookup": "^7.0.0", 54 | "cacheable-request": "^12.0.1", 55 | "decompress-response": "^6.0.0", 56 | "form-data-encoder": "^4.0.2", 57 | "http2-wrapper": "^2.2.1", 58 | "lowercase-keys": "^3.0.0", 59 | "p-cancelable": "^4.0.1", 60 | "responselike": "^3.0.0", 61 | "type-fest": "^4.26.1" 62 | }, 63 | "devDependencies": { 64 | "@hapi/bourne": "^3.0.0", 65 | "@sindresorhus/tsconfig": "^6.0.0", 66 | "@sinonjs/fake-timers": "^11.2.2", 67 | "@types/benchmark": "^2.1.5", 68 | "@types/express": "^5.0.0", 69 | "@types/node": "^22.7.5", 70 | "@types/pem": "^1.14.4", 71 | "@types/readable-stream": "^4.0.14", 72 | "@types/request": "^2.48.12", 73 | "@types/sinon": "^17.0.2", 74 | "@types/sinonjs__fake-timers": "^8.1.5", 75 | "ava": "^5.3.1", 76 | "axios": "^1.7.7", 77 | "benchmark": "^2.1.4", 78 | "bluebird": "^3.7.2", 79 | "body-parser": "^1.20.3", 80 | "create-cert": "^1.0.6", 81 | "create-test-server": "^3.0.1", 82 | "del-cli": "^6.0.0", 83 | "delay": "^6.0.0", 84 | "expect-type": "^1.0.0", 85 | "express": "^4.21.1", 86 | "form-data": "^4.0.0", 87 | "formdata-node": "^6.0.3", 88 | "get-stream": "^9.0.1", 89 | "nock": "^13.5.5", 90 | "node-fetch": "^3.3.2", 91 | "np": "^10.0.5", 92 | "nyc": "^17.1.0", 93 | "p-event": "^6.0.1", 94 | "pem": "^1.14.8", 95 | "pify": "^6.1.0", 96 | "readable-stream": "^4.4.2", 97 | "request": "^2.88.2", 98 | "sinon": "^19.0.2", 99 | "slow-stream": "0.0.4", 100 | "tempy": "^3.1.0", 101 | "then-busboy": "^5.2.1", 102 | "tough-cookie": "^4.1.4", 103 | "tsx": "^4.19.1", 104 | "typescript": "^5.6.3", 105 | "xo": "^0.56.0" 106 | }, 107 | "ava": { 108 | "files": [ 109 | "test/*", 110 | "!test/*.types.ts" 111 | ], 112 | "timeout": "10m", 113 | "extensions": { 114 | "ts": "module" 115 | }, 116 | "workerThreads": false 117 | }, 118 | "nyc": { 119 | "reporter": [ 120 | "text", 121 | "html", 122 | "lcov" 123 | ], 124 | "extension": [ 125 | ".ts" 126 | ], 127 | "exclude": [ 128 | "**/test/**" 129 | ] 130 | }, 131 | "xo": { 132 | "ignores": [ 133 | "documentation/examples/*" 134 | ], 135 | "rules": { 136 | "@typescript-eslint/no-empty-function": "off", 137 | "n/no-deprecated-api": "off", 138 | "@typescript-eslint/no-implicit-any-catch": "off", 139 | "ava/assertion-arguments": "off", 140 | "@typescript-eslint/no-unsafe-member-access": "off", 141 | "@typescript-eslint/no-unsafe-return": "off", 142 | "@typescript-eslint/no-unsafe-assignment": "off", 143 | "@typescript-eslint/no-unsafe-call": "off", 144 | "@typescript-eslint/await-thenable": "off", 145 | "@typescript-eslint/no-redundant-type-constituents": "off", 146 | "@typescript-eslint/no-unsafe-argument": "off", 147 | "@typescript-eslint/promise-function-async": "off", 148 | "no-lone-blocks": "off", 149 | "unicorn/no-await-expression-member": "off", 150 | "unicorn/prefer-event-target": "off" 151 | } 152 | }, 153 | "runkitExampleFilename": "./documentation/examples/runkit-example.js" 154 | } 155 | -------------------------------------------------------------------------------- /source/as-promise/index.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'node:events'; 2 | import is from '@sindresorhus/is'; 3 | import PCancelable from 'p-cancelable'; 4 | import { 5 | HTTPError, 6 | RetryError, 7 | type RequestError, 8 | } from '../core/errors.js'; 9 | import Request from '../core/index.js'; 10 | import { 11 | parseBody, 12 | isResponseOk, 13 | type Response, ParseError, 14 | } from '../core/response.js'; 15 | import proxyEvents from '../core/utils/proxy-events.js'; 16 | import type Options from '../core/options.js'; 17 | import {CancelError, type CancelableRequest} from './types.js'; 18 | 19 | const proxiedRequestEvents = [ 20 | 'request', 21 | 'response', 22 | 'redirect', 23 | 'uploadProgress', 24 | 'downloadProgress', 25 | ]; 26 | 27 | export default function asPromise(firstRequest?: Request): CancelableRequest { 28 | let globalRequest: Request; 29 | let globalResponse: Response; 30 | let normalizedOptions: Options; 31 | const emitter = new EventEmitter(); 32 | 33 | const promise = new PCancelable((resolve, reject, onCancel) => { 34 | onCancel(() => { 35 | globalRequest.destroy(); 36 | }); 37 | 38 | onCancel.shouldReject = false; 39 | onCancel(() => { 40 | reject(new CancelError(globalRequest)); 41 | }); 42 | 43 | const makeRequest = (retryCount: number): void => { 44 | // Errors when a new request is made after the promise settles. 45 | // Used to detect a race condition. 46 | // See https://github.com/sindresorhus/got/issues/1489 47 | onCancel(() => {}); 48 | 49 | const request = firstRequest ?? new Request(undefined, undefined, normalizedOptions); 50 | request.retryCount = retryCount; 51 | request._noPipe = true; 52 | 53 | globalRequest = request; 54 | 55 | request.once('response', async (response: Response) => { 56 | // Parse body 57 | const contentEncoding = (response.headers['content-encoding'] ?? '').toLowerCase(); 58 | const isCompressed = contentEncoding === 'gzip' || contentEncoding === 'deflate' || contentEncoding === 'br'; 59 | 60 | const {options} = request; 61 | 62 | if (isCompressed && !options.decompress) { 63 | response.body = response.rawBody; 64 | } else { 65 | try { 66 | response.body = parseBody(response, options.responseType, options.parseJson, options.encoding); 67 | } catch (error: any) { 68 | // Fall back to `utf8` 69 | try { 70 | response.body = response.rawBody.toString(); 71 | } catch (error) { 72 | request._beforeError(new ParseError(error as Error, response)); 73 | return; 74 | } 75 | 76 | if (isResponseOk(response)) { 77 | request._beforeError(error); 78 | return; 79 | } 80 | } 81 | } 82 | 83 | try { 84 | const hooks = options.hooks.afterResponse; 85 | 86 | for (const [index, hook] of hooks.entries()) { 87 | // @ts-expect-error TS doesn't notice that CancelableRequest is a Promise 88 | // eslint-disable-next-line no-await-in-loop 89 | response = await hook(response, async (updatedOptions): CancelableRequest => { 90 | options.merge(updatedOptions); 91 | options.prefixUrl = ''; 92 | 93 | if (updatedOptions.url) { 94 | options.url = updatedOptions.url; 95 | } 96 | 97 | // Remove any further hooks for that request, because we'll call them anyway. 98 | // The loop continues. We don't want duplicates (asPromise recursion). 99 | options.hooks.afterResponse = options.hooks.afterResponse.slice(0, index); 100 | 101 | throw new RetryError(request); 102 | }); 103 | 104 | if (!(is.object(response) && is.number(response.statusCode) && !is.nullOrUndefined(response.body))) { 105 | throw new TypeError('The `afterResponse` hook returned an invalid value'); 106 | } 107 | } 108 | } catch (error: any) { 109 | request._beforeError(error); 110 | return; 111 | } 112 | 113 | globalResponse = response; 114 | 115 | if (!isResponseOk(response)) { 116 | request._beforeError(new HTTPError(response)); 117 | return; 118 | } 119 | 120 | request.destroy(); 121 | resolve(request.options.resolveBodyOnly ? response.body as T : response as unknown as T); 122 | }); 123 | 124 | const onError = (error: RequestError) => { 125 | if (promise.isCanceled) { 126 | return; 127 | } 128 | 129 | const {options} = request; 130 | 131 | if (error instanceof HTTPError && !options.throwHttpErrors) { 132 | const {response} = error; 133 | 134 | request.destroy(); 135 | resolve(request.options.resolveBodyOnly ? response.body as T : response as unknown as T); 136 | return; 137 | } 138 | 139 | reject(error); 140 | }; 141 | 142 | request.once('error', onError); 143 | 144 | const previousBody = request.options?.body; 145 | 146 | request.once('retry', (newRetryCount: number, error: RequestError) => { 147 | firstRequest = undefined; 148 | 149 | const newBody = request.options.body; 150 | 151 | if (previousBody === newBody && is.nodeStream(newBody)) { 152 | error.message = 'Cannot retry with consumed body stream'; 153 | 154 | onError(error); 155 | return; 156 | } 157 | 158 | // This is needed! We need to reuse `request.options` because they can get modified! 159 | // For example, by calling `promise.json()`. 160 | normalizedOptions = request.options; 161 | 162 | makeRequest(newRetryCount); 163 | }); 164 | 165 | proxyEvents(request, emitter, proxiedRequestEvents); 166 | 167 | if (is.undefined(firstRequest)) { 168 | void request.flush(); 169 | } 170 | }; 171 | 172 | makeRequest(0); 173 | }) as CancelableRequest; 174 | 175 | promise.on = (event: string, function_: (...arguments_: any[]) => void) => { 176 | emitter.on(event, function_); 177 | return promise; 178 | }; 179 | 180 | promise.off = (event: string, function_: (...arguments_: any[]) => void) => { 181 | emitter.off(event, function_); 182 | return promise; 183 | }; 184 | 185 | const shortcut = (responseType: Options['responseType']): CancelableRequest => { 186 | const newPromise = (async () => { 187 | // Wait until downloading has ended 188 | await promise; 189 | 190 | const {options} = globalResponse.request; 191 | 192 | return parseBody(globalResponse, responseType, options.parseJson, options.encoding); 193 | })(); 194 | 195 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 196 | Object.defineProperties(newPromise, Object.getOwnPropertyDescriptors(promise)); 197 | 198 | return newPromise as CancelableRequest; 199 | }; 200 | 201 | promise.json = () => { 202 | if (globalRequest.options) { 203 | const {headers} = globalRequest.options; 204 | 205 | if (!globalRequest.writableFinished && !('accept' in headers)) { 206 | headers.accept = 'application/json'; 207 | } 208 | } 209 | 210 | return shortcut('json'); 211 | }; 212 | 213 | promise.buffer = () => shortcut('buffer'); 214 | promise.text = () => shortcut('text'); 215 | 216 | return promise; 217 | } 218 | -------------------------------------------------------------------------------- /source/as-promise/types.ts: -------------------------------------------------------------------------------- 1 | import type {Buffer} from 'node:buffer'; 2 | import type PCancelable from 'p-cancelable'; 3 | import {RequestError} from '../core/errors.js'; 4 | import type Request from '../core/index.js'; 5 | import {type RequestEvents} from '../core/index.js'; 6 | import type {Response} from '../core/response.js'; 7 | 8 | /** 9 | An error to be thrown when the request is aborted with `.cancel()`. 10 | */ 11 | export class CancelError extends RequestError { 12 | declare readonly response: Response; 13 | 14 | constructor(request: Request) { 15 | super('Promise was canceled', {}, request); 16 | this.name = 'CancelError'; 17 | this.code = 'ERR_CANCELED'; 18 | } 19 | 20 | /** 21 | Whether the promise is canceled. 22 | */ 23 | get isCanceled() { 24 | return true; 25 | } 26 | } 27 | 28 | // TODO: Make this a `type`. 29 | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- TS cannot handle this being a `type` for some reason. 30 | export interface CancelableRequest extends PCancelable, RequestEvents> { 31 | /** 32 | A shortcut method that gives a Promise returning a JSON object. 33 | 34 | It is semantically the same as settings `options.resolveBodyOnly` to `true` and `options.responseType` to `'json'`. 35 | */ 36 | json: () => CancelableRequest; 37 | 38 | /** 39 | A shortcut method that gives a Promise returning a [Buffer](https://nodejs.org/api/buffer.html). 40 | 41 | It is semantically the same as settings `options.resolveBodyOnly` to `true` and `options.responseType` to `'buffer'`. 42 | */ 43 | buffer: () => CancelableRequest; 44 | 45 | /** 46 | A shortcut method that gives a Promise returning a string. 47 | 48 | It is semantically the same as settings `options.resolveBodyOnly` to `true` and `options.responseType` to `'text'`. 49 | */ 50 | text: () => CancelableRequest; 51 | } 52 | -------------------------------------------------------------------------------- /source/core/calculate-retry-delay.ts: -------------------------------------------------------------------------------- 1 | import type {RetryFunction} from './options.js'; 2 | 3 | type Returns unknown, V> = (...arguments_: Parameters) => V; 4 | 5 | const calculateRetryDelay: Returns = ({ 6 | attemptCount, 7 | retryOptions, 8 | error, 9 | retryAfter, 10 | computedValue, 11 | }) => { 12 | if (error.name === 'RetryError') { 13 | return 1; 14 | } 15 | 16 | if (attemptCount > retryOptions.limit) { 17 | return 0; 18 | } 19 | 20 | const hasMethod = retryOptions.methods.includes(error.options.method); 21 | const hasErrorCode = retryOptions.errorCodes.includes(error.code); 22 | const hasStatusCode = error.response && retryOptions.statusCodes.includes(error.response.statusCode); 23 | if (!hasMethod || (!hasErrorCode && !hasStatusCode)) { 24 | return 0; 25 | } 26 | 27 | if (error.response) { 28 | if (retryAfter) { 29 | // In this case `computedValue` is `options.request.timeout` 30 | if (retryAfter > computedValue) { 31 | return 0; 32 | } 33 | 34 | return retryAfter; 35 | } 36 | 37 | if (error.response.statusCode === 413) { 38 | return 0; 39 | } 40 | } 41 | 42 | const noise = Math.random() * retryOptions.noise; 43 | return Math.min(((2 ** (attemptCount - 1)) * 1000), retryOptions.backoffLimit) + noise; 44 | }; 45 | 46 | export default calculateRetryDelay; 47 | -------------------------------------------------------------------------------- /source/core/errors.ts: -------------------------------------------------------------------------------- 1 | import is from '@sindresorhus/is'; 2 | import type {Timings} from '@szmarczak/http-timer'; 3 | import type Options from './options.js'; 4 | import type {TimeoutError as TimedOutTimeoutError} from './timed-out.js'; 5 | import type {PlainResponse, Response} from './response.js'; 6 | import type Request from './index.js'; 7 | 8 | type Error = NodeJS.ErrnoException; 9 | 10 | // A hacky check to prevent circular references. 11 | function isRequest(x: unknown): x is Request { 12 | return is.object(x) && '_onResponse' in x; 13 | } 14 | 15 | /** 16 | An error to be thrown when a request fails. 17 | Contains a `code` property with error class code, like `ECONNREFUSED`. 18 | */ 19 | export class RequestError extends Error { 20 | input?: string; 21 | 22 | code: string; 23 | override stack!: string; 24 | declare readonly options: Options; 25 | readonly response?: Response; 26 | readonly request?: Request; 27 | readonly timings?: Timings; 28 | 29 | constructor(message: string, error: Partial, self: Request | Options) { 30 | super(message, {cause: error}); 31 | Error.captureStackTrace(this, this.constructor); 32 | 33 | this.name = 'RequestError'; 34 | this.code = error.code ?? 'ERR_GOT_REQUEST_ERROR'; 35 | this.input = (error as any).input; 36 | 37 | if (isRequest(self)) { 38 | Object.defineProperty(this, 'request', { 39 | enumerable: false, 40 | value: self, 41 | }); 42 | 43 | Object.defineProperty(this, 'response', { 44 | enumerable: false, 45 | value: self.response, 46 | }); 47 | 48 | this.options = self.options; 49 | } else { 50 | this.options = self; 51 | } 52 | 53 | this.timings = this.request?.timings; 54 | 55 | // Recover the original stacktrace 56 | if (is.string(error.stack) && is.string(this.stack)) { 57 | const indexOfMessage = this.stack.indexOf(this.message) + this.message.length; 58 | const thisStackTrace = this.stack.slice(indexOfMessage).split('\n').reverse(); 59 | const errorStackTrace = error.stack.slice(error.stack.indexOf(error.message!) + error.message!.length).split('\n').reverse(); 60 | 61 | // Remove duplicated traces 62 | while (errorStackTrace.length > 0 && errorStackTrace[0] === thisStackTrace[0]) { 63 | thisStackTrace.shift(); 64 | } 65 | 66 | this.stack = `${this.stack.slice(0, indexOfMessage)}${thisStackTrace.reverse().join('\n')}${errorStackTrace.reverse().join('\n')}`; 67 | } 68 | } 69 | } 70 | 71 | /** 72 | An error to be thrown when the server redirects you more than ten times. 73 | Includes a `response` property. 74 | */ 75 | export class MaxRedirectsError extends RequestError { 76 | declare readonly response: Response; 77 | declare readonly request: Request; 78 | declare readonly timings: Timings; 79 | 80 | constructor(request: Request) { 81 | super(`Redirected ${request.options.maxRedirects} times. Aborting.`, {}, request); 82 | this.name = 'MaxRedirectsError'; 83 | this.code = 'ERR_TOO_MANY_REDIRECTS'; 84 | } 85 | } 86 | 87 | /** 88 | 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. 89 | Includes a `response` property. 90 | */ 91 | // TODO: Change `HTTPError` to `HTTPError` in the next major version to enforce type usage. 92 | // eslint-disable-next-line @typescript-eslint/naming-convention 93 | export class HTTPError extends RequestError { 94 | declare readonly response: Response; 95 | declare readonly request: Request; 96 | declare readonly timings: Timings; 97 | 98 | constructor(response: PlainResponse) { 99 | super(`Response code ${response.statusCode} (${response.statusMessage!})`, {}, response.request); 100 | this.name = 'HTTPError'; 101 | this.code = 'ERR_NON_2XX_3XX_RESPONSE'; 102 | } 103 | } 104 | 105 | /** 106 | An error to be thrown when a cache method fails. 107 | For example, if the database goes down or there's a filesystem error. 108 | */ 109 | export class CacheError extends RequestError { 110 | declare readonly request: Request; 111 | 112 | constructor(error: Error, request: Request) { 113 | super(error.message, error, request); 114 | this.name = 'CacheError'; 115 | this.code = this.code === 'ERR_GOT_REQUEST_ERROR' ? 'ERR_CACHE_ACCESS' : this.code; 116 | } 117 | } 118 | 119 | /** 120 | An error to be thrown when the request body is a stream and an error occurs while reading from that stream. 121 | */ 122 | export class UploadError extends RequestError { 123 | declare readonly request: Request; 124 | 125 | constructor(error: Error, request: Request) { 126 | super(error.message, error, request); 127 | this.name = 'UploadError'; 128 | this.code = this.code === 'ERR_GOT_REQUEST_ERROR' ? 'ERR_UPLOAD' : this.code; 129 | } 130 | } 131 | 132 | /** 133 | An error to be thrown when the request is aborted due to a timeout. 134 | Includes an `event` and `timings` property. 135 | */ 136 | export class TimeoutError extends RequestError { 137 | declare readonly request: Request; 138 | override readonly timings: Timings; 139 | readonly event: string; 140 | 141 | constructor(error: TimedOutTimeoutError, timings: Timings, request: Request) { 142 | super(error.message, error, request); 143 | this.name = 'TimeoutError'; 144 | this.event = error.event; 145 | this.timings = timings; 146 | } 147 | } 148 | 149 | /** 150 | An error to be thrown when reading from response stream fails. 151 | */ 152 | export class ReadError extends RequestError { 153 | declare readonly request: Request; 154 | declare readonly response: Response; 155 | declare readonly timings: Timings; 156 | 157 | constructor(error: Error, request: Request) { 158 | super(error.message, error, request); 159 | this.name = 'ReadError'; 160 | this.code = this.code === 'ERR_GOT_REQUEST_ERROR' ? 'ERR_READING_RESPONSE_STREAM' : this.code; 161 | } 162 | } 163 | 164 | /** 165 | An error which always triggers a new retry when thrown. 166 | */ 167 | export class RetryError extends RequestError { 168 | constructor(request: Request) { 169 | super('Retrying', {}, request); 170 | this.name = 'RetryError'; 171 | this.code = 'ERR_RETRYING'; 172 | } 173 | } 174 | 175 | /** 176 | An error to be thrown when the request is aborted by AbortController. 177 | */ 178 | export class AbortError extends RequestError { 179 | constructor(request: Request) { 180 | super('This operation was aborted.', {}, request); 181 | this.code = 'ERR_ABORTED'; 182 | this.name = 'AbortError'; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /source/core/parse-link-header.ts: -------------------------------------------------------------------------------- 1 | export default function parseLinkHeader(link: string) { 2 | const parsed = []; 3 | 4 | const items = link.split(','); 5 | 6 | for (const item of items) { 7 | // https://tools.ietf.org/html/rfc5988#section-5 8 | const [rawUriReference, ...rawLinkParameters] = item.split(';') as [string, ...string[]]; 9 | const trimmedUriReference = rawUriReference.trim(); 10 | 11 | // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with 12 | if (trimmedUriReference[0] !== '<' || trimmedUriReference.at(-1) !== '>') { 13 | throw new Error(`Invalid format of the Link header reference: ${trimmedUriReference}`); 14 | } 15 | 16 | const reference = trimmedUriReference.slice(1, -1); 17 | const parameters: Record = {}; 18 | 19 | if (rawLinkParameters.length === 0) { 20 | throw new Error(`Unexpected end of Link header parameters: ${rawLinkParameters.join(';')}`); 21 | } 22 | 23 | for (const rawParameter of rawLinkParameters) { 24 | const trimmedRawParameter = rawParameter.trim(); 25 | const center = trimmedRawParameter.indexOf('='); 26 | 27 | if (center === -1) { 28 | throw new Error(`Failed to parse Link header: ${link}`); 29 | } 30 | 31 | const name = trimmedRawParameter.slice(0, center).trim(); 32 | const value = trimmedRawParameter.slice(center + 1).trim(); 33 | 34 | parameters[name] = value; 35 | } 36 | 37 | parsed.push({ 38 | reference, 39 | parameters, 40 | }); 41 | } 42 | 43 | return parsed; 44 | } 45 | -------------------------------------------------------------------------------- /source/core/response.ts: -------------------------------------------------------------------------------- 1 | import type {Buffer} from 'node:buffer'; 2 | import type {IncomingMessageWithTimings, Timings} from '@szmarczak/http-timer'; 3 | import {RequestError} from './errors.js'; 4 | import type {ParseJsonFunction, ResponseType} from './options.js'; 5 | import type Request from './index.js'; 6 | 7 | export type PlainResponse = { 8 | /** 9 | The original request URL. 10 | */ 11 | requestUrl: URL; 12 | 13 | /** 14 | The redirect URLs. 15 | */ 16 | redirectUrls: URL[]; 17 | 18 | /** 19 | - `options` - The Got options that were set on this request. 20 | 21 | __Note__: This is not a [http.ClientRequest](https://nodejs.org/api/http.html#http_class_http_clientrequest). 22 | */ 23 | request: Request; 24 | 25 | /** 26 | The remote IP address. 27 | 28 | This is hopefully a temporary limitation, see [lukechilds/cacheable-request#86](https://web.archive.org/web/20220804165050/https://github.com/jaredwray/cacheable-request/issues/86). 29 | 30 | __Note__: Not available when the response is cached. 31 | */ 32 | ip?: string; 33 | 34 | /** 35 | Whether the response was retrieved from the cache. 36 | */ 37 | isFromCache: boolean; 38 | 39 | /** 40 | The status code of the response. 41 | */ 42 | statusCode: number; 43 | 44 | /** 45 | The request URL or the final URL after redirects. 46 | */ 47 | url: string; 48 | 49 | /** 50 | The object contains the following properties: 51 | 52 | - `start` - Time when the request started. 53 | - `socket` - Time when a socket was assigned to the request. 54 | - `lookup` - Time when the DNS lookup finished. 55 | - `connect` - Time when the socket successfully connected. 56 | - `secureConnect` - Time when the socket securely connected. 57 | - `upload` - Time when the request finished uploading. 58 | - `response` - Time when the request fired `response` event. 59 | - `end` - Time when the response fired `end` event. 60 | - `error` - Time when the request fired `error` event. 61 | - `abort` - Time when the request fired `abort` event. 62 | - `phases` 63 | - `wait` - `timings.socket - timings.start` 64 | - `dns` - `timings.lookup - timings.socket` 65 | - `tcp` - `timings.connect - timings.lookup` 66 | - `tls` - `timings.secureConnect - timings.connect` 67 | - `request` - `timings.upload - (timings.secureConnect || timings.connect)` 68 | - `firstByte` - `timings.response - timings.upload` 69 | - `download` - `timings.end - timings.response` 70 | - `total` - `(timings.end || timings.error || timings.abort) - timings.start` 71 | 72 | If something has not been measured yet, it will be `undefined`. 73 | 74 | __Note__: The time is a `number` representing the milliseconds elapsed since the UNIX epoch. 75 | */ 76 | timings: Timings; 77 | 78 | /** 79 | The number of times the request was retried. 80 | */ 81 | retryCount: number; 82 | 83 | // Defined only if request errored 84 | /** 85 | The raw result of the request. 86 | */ 87 | rawBody?: Buffer; 88 | 89 | /** 90 | The result of the request. 91 | */ 92 | body?: unknown; 93 | 94 | /** 95 | Whether the response was successful. 96 | 97 | __Note__: Got throws automatically when `response.ok` is `false` and `throwHttpErrors` is `true`. 98 | */ 99 | ok: boolean; 100 | } & IncomingMessageWithTimings; 101 | 102 | // For Promise support 103 | export type Response = { 104 | /** 105 | The result of the request. 106 | */ 107 | body: T; 108 | 109 | /** 110 | The raw result of the request. 111 | */ 112 | rawBody: Buffer; 113 | } & PlainResponse; 114 | 115 | export const isResponseOk = (response: PlainResponse): boolean => { 116 | const {statusCode} = response; 117 | const {followRedirect} = response.request.options; 118 | const shouldFollow = typeof followRedirect === 'function' ? followRedirect(response) : followRedirect; 119 | const limitStatusCode = shouldFollow ? 299 : 399; 120 | 121 | return (statusCode >= 200 && statusCode <= limitStatusCode) || statusCode === 304; 122 | }; 123 | 124 | /** 125 | An error to be thrown when server response code is 2xx, and parsing body fails. 126 | Includes a `response` property. 127 | */ 128 | export class ParseError extends RequestError { 129 | declare readonly response: Response; 130 | 131 | constructor(error: Error, response: Response) { 132 | const {options} = response.request; 133 | 134 | super(`${error.message} in "${options.url!.toString()}"`, error, response.request); 135 | this.name = 'ParseError'; 136 | this.code = 'ERR_BODY_PARSE_FAILURE'; 137 | } 138 | } 139 | 140 | export const parseBody = (response: Response, responseType: ResponseType, parseJson: ParseJsonFunction, encoding?: BufferEncoding): unknown => { 141 | const {rawBody} = response; 142 | 143 | try { 144 | if (responseType === 'text') { 145 | return rawBody.toString(encoding); 146 | } 147 | 148 | if (responseType === 'json') { 149 | return rawBody.length === 0 ? '' : parseJson(rawBody.toString(encoding)); 150 | } 151 | 152 | if (responseType === 'buffer') { 153 | return rawBody; 154 | } 155 | } catch (error) { 156 | throw new ParseError(error as Error, response); 157 | } 158 | 159 | throw new ParseError({ 160 | message: `Unknown body type '${responseType as string}'`, 161 | name: 'Error', 162 | }, response); 163 | }; 164 | -------------------------------------------------------------------------------- /source/core/timed-out.ts: -------------------------------------------------------------------------------- 1 | import net from 'node:net'; 2 | import type {ClientRequest, IncomingMessage} from 'node:http'; 3 | import unhandler from './utils/unhandle.js'; 4 | 5 | const reentry: unique symbol = Symbol('reentry'); 6 | const noop = (): void => {}; 7 | 8 | type TimedOutOptions = { 9 | host?: string; 10 | hostname?: string; 11 | protocol?: string; 12 | }; 13 | 14 | export type Delays = { 15 | lookup?: number; 16 | socket?: number; 17 | connect?: number; 18 | secureConnect?: number; 19 | send?: number; 20 | response?: number; 21 | read?: number; 22 | request?: number; 23 | }; 24 | 25 | export type ErrorCode = 26 | | 'ETIMEDOUT' 27 | | 'ECONNRESET' 28 | | 'EADDRINUSE' 29 | | 'ECONNREFUSED' 30 | | 'EPIPE' 31 | | 'ENOTFOUND' 32 | | 'ENETUNREACH' 33 | | 'EAI_AGAIN'; 34 | 35 | export class TimeoutError extends Error { 36 | code: ErrorCode; 37 | 38 | constructor(threshold: number, public event: string) { 39 | super(`Timeout awaiting '${event}' for ${threshold}ms`); 40 | 41 | this.name = 'TimeoutError'; 42 | this.code = 'ETIMEDOUT'; 43 | } 44 | } 45 | 46 | export default function timedOut(request: ClientRequest, delays: Delays, options: TimedOutOptions): () => void { 47 | if (reentry in request) { 48 | return noop; 49 | } 50 | 51 | request[reentry] = true; 52 | const cancelers: Array = []; 53 | const {once, unhandleAll} = unhandler(); 54 | const handled: Map = new Map(); 55 | 56 | const addTimeout = (delay: number, callback: (delay: number, event: string) => void, event: string): (typeof noop) => { 57 | const timeout = setTimeout(callback, delay, delay, event) as unknown as NodeJS.Timeout; 58 | 59 | timeout.unref?.(); 60 | 61 | const cancel = (): void => { 62 | handled.set(event, true); 63 | clearTimeout(timeout); 64 | }; 65 | 66 | cancelers.push(cancel); 67 | 68 | return cancel; 69 | }; 70 | 71 | const {host, hostname} = options; 72 | 73 | const timeoutHandler = (delay: number, event: string): void => { 74 | // Use setTimeout to allow for any cancelled events to be handled first, 75 | // to prevent firing any TimeoutError unneeded when the event loop is busy or blocked 76 | setTimeout(() => { 77 | if (!handled.has(event)) { 78 | request.destroy(new TimeoutError(delay, event)); 79 | } 80 | }, 0); 81 | }; 82 | 83 | const cancelTimeouts = (): void => { 84 | for (const cancel of cancelers) { 85 | cancel(); 86 | } 87 | 88 | unhandleAll(); 89 | }; 90 | 91 | request.once('error', error => { 92 | cancelTimeouts(); 93 | 94 | // Save original behavior 95 | /* istanbul ignore next */ 96 | if (request.listenerCount('error') === 0) { 97 | throw error; 98 | } 99 | }); 100 | 101 | if (delays.request !== undefined) { 102 | const cancelTimeout = addTimeout(delays.request, timeoutHandler, 'request'); 103 | 104 | once(request, 'response', (response: IncomingMessage): void => { 105 | once(response, 'end', cancelTimeout); 106 | }); 107 | } 108 | 109 | if (delays.socket !== undefined) { 110 | const {socket} = delays; 111 | 112 | const socketTimeoutHandler = (): void => { 113 | timeoutHandler(socket, 'socket'); 114 | }; 115 | 116 | request.setTimeout(socket, socketTimeoutHandler); 117 | 118 | // `request.setTimeout(0)` causes a memory leak. 119 | // We can just remove the listener and forget about the timer - it's unreffed. 120 | // See https://github.com/sindresorhus/got/issues/690 121 | cancelers.push(() => { 122 | request.removeListener('timeout', socketTimeoutHandler); 123 | }); 124 | } 125 | 126 | const hasLookup = delays.lookup !== undefined; 127 | const hasConnect = delays.connect !== undefined; 128 | const hasSecureConnect = delays.secureConnect !== undefined; 129 | const hasSend = delays.send !== undefined; 130 | if (hasLookup || hasConnect || hasSecureConnect || hasSend) { 131 | once(request, 'socket', (socket: net.Socket): void => { 132 | const {socketPath} = request as ClientRequest & {socketPath?: string}; 133 | 134 | /* istanbul ignore next: hard to test */ 135 | if (socket.connecting) { 136 | const hasPath = Boolean(socketPath ?? net.isIP(hostname ?? host ?? '') !== 0); 137 | 138 | if (hasLookup && !hasPath && (socket.address() as net.AddressInfo).address === undefined) { 139 | const cancelTimeout = addTimeout(delays.lookup!, timeoutHandler, 'lookup'); 140 | once(socket, 'lookup', cancelTimeout); 141 | } 142 | 143 | if (hasConnect) { 144 | const timeConnect = (): (() => void) => addTimeout(delays.connect!, timeoutHandler, 'connect'); 145 | 146 | if (hasPath) { 147 | once(socket, 'connect', timeConnect()); 148 | } else { 149 | once(socket, 'lookup', (error: Error): void => { 150 | if (error === null) { 151 | once(socket, 'connect', timeConnect()); 152 | } 153 | }); 154 | } 155 | } 156 | 157 | if (hasSecureConnect && options.protocol === 'https:') { 158 | once(socket, 'connect', (): void => { 159 | const cancelTimeout = addTimeout(delays.secureConnect!, timeoutHandler, 'secureConnect'); 160 | once(socket, 'secureConnect', cancelTimeout); 161 | }); 162 | } 163 | } 164 | 165 | if (hasSend) { 166 | const timeRequest = (): (() => void) => addTimeout(delays.send!, timeoutHandler, 'send'); 167 | /* istanbul ignore next: hard to test */ 168 | if (socket.connecting) { 169 | once(socket, 'connect', (): void => { 170 | once(request, 'upload-complete', timeRequest()); 171 | }); 172 | } else { 173 | once(request, 'upload-complete', timeRequest()); 174 | } 175 | } 176 | }); 177 | } 178 | 179 | if (delays.response !== undefined) { 180 | once(request, 'upload-complete', (): void => { 181 | const cancelTimeout = addTimeout(delays.response!, timeoutHandler, 'response'); 182 | once(request, 'response', cancelTimeout); 183 | }); 184 | } 185 | 186 | if (delays.read !== undefined) { 187 | once(request, 'response', (response: IncomingMessage): void => { 188 | const cancelTimeout = addTimeout(delays.read!, timeoutHandler, 'read'); 189 | once(response, 'end', cancelTimeout); 190 | }); 191 | } 192 | 193 | return cancelTimeouts; 194 | } 195 | 196 | declare module 'http' { 197 | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- This has to be an `interface` to be able to be merged. 198 | interface ClientRequest { 199 | [reentry]?: boolean; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /source/core/utils/get-body-size.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'node:buffer'; 2 | import {promisify} from 'node:util'; 3 | import type {ClientRequestArgs} from 'node:http'; 4 | import is from '@sindresorhus/is'; 5 | import isFormData from './is-form-data.js'; 6 | 7 | export default async function getBodySize(body: unknown, headers: ClientRequestArgs['headers']): Promise { 8 | if (headers && 'content-length' in headers) { 9 | return Number(headers['content-length']); 10 | } 11 | 12 | if (!body) { 13 | return 0; 14 | } 15 | 16 | if (is.string(body)) { 17 | return Buffer.byteLength(body); 18 | } 19 | 20 | if (is.buffer(body)) { 21 | return body.length; 22 | } 23 | 24 | if (isFormData(body)) { 25 | return promisify(body.getLength.bind(body))(); 26 | } 27 | 28 | return undefined; 29 | } 30 | -------------------------------------------------------------------------------- /source/core/utils/is-client-request.ts: -------------------------------------------------------------------------------- 1 | import type {Writable, Readable} from 'node:stream'; 2 | import type {ClientRequest} from 'node:http'; 3 | 4 | function isClientRequest(clientRequest: Writable | Readable): clientRequest is ClientRequest { 5 | return (clientRequest as Writable).writable && !(clientRequest as Writable).writableEnded; 6 | } 7 | 8 | export default isClientRequest; 9 | -------------------------------------------------------------------------------- /source/core/utils/is-form-data.ts: -------------------------------------------------------------------------------- 1 | import type {Readable} from 'node:stream'; 2 | import is from '@sindresorhus/is'; 3 | 4 | type FormData = { 5 | getBoundary: () => string; 6 | getLength: (callback: (error: Error | null, length: number) => void) => void; // eslint-disable-line @typescript-eslint/ban-types 7 | } & Readable; 8 | 9 | export default function isFormData(body: unknown): body is FormData { 10 | return is.nodeStream(body) && is.function((body as FormData).getBoundary); 11 | } 12 | -------------------------------------------------------------------------------- /source/core/utils/is-unix-socket-url.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/naming-convention 2 | export default function isUnixSocketURL(url: URL) { 3 | return url.protocol === 'unix:' || url.hostname === 'unix'; 4 | } 5 | -------------------------------------------------------------------------------- /source/core/utils/options-to-url.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/naming-convention 2 | export type URLOptions = { 3 | href?: string; 4 | protocol?: string; 5 | host?: string; 6 | hostname?: string; 7 | port?: string | number; 8 | pathname?: string; 9 | search?: string; 10 | searchParams?: unknown; 11 | path?: string; 12 | }; 13 | 14 | const keys: Array> = [ 15 | 'protocol', 16 | 'host', 17 | 'hostname', 18 | 'port', 19 | 'pathname', 20 | 'search', 21 | ]; 22 | 23 | export default function optionsToUrl(origin: string, options: URLOptions): URL { 24 | if (options.path) { 25 | if (options.pathname) { 26 | throw new TypeError('Parameters `path` and `pathname` are mutually exclusive.'); 27 | } 28 | 29 | if (options.search) { 30 | throw new TypeError('Parameters `path` and `search` are mutually exclusive.'); 31 | } 32 | 33 | if (options.searchParams) { 34 | throw new TypeError('Parameters `path` and `searchParams` are mutually exclusive.'); 35 | } 36 | } 37 | 38 | if (options.search && options.searchParams) { 39 | throw new TypeError('Parameters `search` and `searchParams` are mutually exclusive.'); 40 | } 41 | 42 | if (!origin) { 43 | if (!options.protocol) { 44 | throw new TypeError('No URL protocol specified'); 45 | } 46 | 47 | origin = `${options.protocol}//${options.hostname ?? options.host ?? ''}`; 48 | } 49 | 50 | const url = new URL(origin); 51 | 52 | if (options.path) { 53 | const searchIndex = options.path.indexOf('?'); 54 | if (searchIndex === -1) { 55 | options.pathname = options.path; 56 | } else { 57 | options.pathname = options.path.slice(0, searchIndex); 58 | options.search = options.path.slice(searchIndex + 1); 59 | } 60 | 61 | delete options.path; 62 | } 63 | 64 | for (const key of keys) { 65 | if (options[key]) { 66 | url[key] = options[key].toString(); 67 | } 68 | } 69 | 70 | return url; 71 | } 72 | -------------------------------------------------------------------------------- /source/core/utils/proxy-events.ts: -------------------------------------------------------------------------------- 1 | import type {EventEmitter} from 'node:events'; 2 | 3 | type AnyFunction = (...arguments_: unknown[]) => void; 4 | type Functions = Record; 5 | 6 | export default function proxyEvents(from: EventEmitter, to: EventEmitter, events: Readonly): () => void { 7 | const eventFunctions: Functions = {}; 8 | 9 | for (const event of events) { 10 | const eventFunction = (...arguments_: unknown[]) => { 11 | to.emit(event, ...arguments_); 12 | }; 13 | 14 | eventFunctions[event] = eventFunction; 15 | 16 | from.on(event, eventFunction); 17 | } 18 | 19 | return () => { 20 | for (const [event, eventFunction] of Object.entries(eventFunctions)) { 21 | from.off(event, eventFunction); 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /source/core/utils/unhandle.ts: -------------------------------------------------------------------------------- 1 | import type {EventEmitter} from 'node:events'; 2 | 3 | type Origin = EventEmitter; 4 | type Event = string | symbol; 5 | type AnyFunction = (...arguments_: any[]) => void; 6 | 7 | type Handler = { 8 | origin: Origin; 9 | event: Event; 10 | fn: AnyFunction; 11 | }; 12 | 13 | type Unhandler = { 14 | once: (origin: Origin, event: Event, function_: AnyFunction) => void; 15 | unhandleAll: () => void; 16 | }; 17 | 18 | // When attaching listeners, it's very easy to forget about them. 19 | // Especially if you do error handling and set timeouts. 20 | // So instead of checking if it's proper to throw an error on every timeout ever, 21 | // use this simple tool which will remove all listeners you have attached. 22 | export default function unhandle(): Unhandler { 23 | const handlers: Handler[] = []; 24 | 25 | return { 26 | once(origin: Origin, event: Event, function_: AnyFunction) { 27 | origin.once(event, function_); 28 | handlers.push({origin, event, fn: function_}); 29 | }, 30 | 31 | unhandleAll() { 32 | for (const handler of handlers) { 33 | const {origin, event, fn} = handler; 34 | origin.removeListener(event, fn); 35 | } 36 | 37 | handlers.length = 0; 38 | }, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /source/core/utils/url-to-options.ts: -------------------------------------------------------------------------------- 1 | import type {UrlWithStringQuery} from 'node:url'; 2 | import is from '@sindresorhus/is'; 3 | 4 | // TODO: Deprecate legacy URL at some point 5 | 6 | export type LegacyUrlOptions = { 7 | protocol: string; 8 | hostname: string; 9 | host: string; 10 | hash: string | null; // eslint-disable-line @typescript-eslint/ban-types 11 | search: string | null; // eslint-disable-line @typescript-eslint/ban-types 12 | pathname: string; 13 | href: string; 14 | path: string; 15 | port?: number; 16 | auth?: string; 17 | }; 18 | 19 | export default function urlToOptions(url: URL | UrlWithStringQuery): LegacyUrlOptions { 20 | // Cast to URL 21 | url = url as URL; 22 | 23 | const options: LegacyUrlOptions = { 24 | protocol: url.protocol, 25 | hostname: is.string(url.hostname) && url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname, 26 | host: url.host, 27 | hash: url.hash, 28 | search: url.search, 29 | pathname: url.pathname, 30 | href: url.href, 31 | path: `${url.pathname || ''}${url.search || ''}`, 32 | }; 33 | 34 | if (is.string(url.port) && url.port.length > 0) { 35 | options.port = Number(url.port); 36 | } 37 | 38 | if (url.username || url.password) { 39 | options.auth = `${url.username || ''}:${url.password || ''}`; 40 | } 41 | 42 | return options; 43 | } 44 | -------------------------------------------------------------------------------- /source/core/utils/weakable-map.ts: -------------------------------------------------------------------------------- 1 | export default class WeakableMap { 2 | weakMap: WeakMap, V>; 3 | map: Map; 4 | 5 | constructor() { 6 | this.weakMap = new WeakMap(); 7 | this.map = new Map(); 8 | } 9 | 10 | set(key: K, value: V): void { 11 | if (typeof key === 'object') { 12 | this.weakMap.set(key as unknown as Record, value); 13 | } else { 14 | this.map.set(key, value); 15 | } 16 | } 17 | 18 | get(key: K): V | undefined { 19 | if (typeof key === 'object') { 20 | return this.weakMap.get(key as unknown as Record); 21 | } 22 | 23 | return this.map.get(key); 24 | } 25 | 26 | has(key: K): boolean { 27 | if (typeof key === 'object') { 28 | return this.weakMap.has(key as unknown as Record); 29 | } 30 | 31 | return this.map.has(key); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /source/create.ts: -------------------------------------------------------------------------------- 1 | import {setTimeout as delay} from 'node:timers/promises'; 2 | import is, {assert} from '@sindresorhus/is'; 3 | import asPromise from './as-promise/index.js'; 4 | import type { 5 | GotReturn, 6 | ExtendOptions, 7 | Got, 8 | HTTPAlias, 9 | InstanceDefaults, 10 | GotPaginate, 11 | GotStream, 12 | GotRequestFunction, 13 | OptionsWithPagination, 14 | StreamOptions, 15 | } from './types.js'; 16 | import Request from './core/index.js'; 17 | import type {Response} from './core/response.js'; 18 | import Options, {type OptionsInit} from './core/options.js'; 19 | import type {CancelableRequest} from './as-promise/types.js'; 20 | 21 | const isGotInstance = (value: Got | ExtendOptions): value is Got => is.function(value); 22 | 23 | const aliases: readonly HTTPAlias[] = [ 24 | 'get', 25 | 'post', 26 | 'put', 27 | 'patch', 28 | 'head', 29 | 'delete', 30 | ]; 31 | 32 | const create = (defaults: InstanceDefaults): Got => { 33 | defaults = { 34 | options: new Options(undefined, undefined, defaults.options), 35 | handlers: [...defaults.handlers], 36 | mutableDefaults: defaults.mutableDefaults, 37 | }; 38 | 39 | Object.defineProperty(defaults, 'mutableDefaults', { 40 | enumerable: true, 41 | configurable: false, 42 | writable: false, 43 | }); 44 | 45 | // Got interface 46 | const got: Got = ((url: string | URL | OptionsInit | undefined, options?: OptionsInit, defaultOptions: Options = defaults.options): GotReturn => { 47 | const request = new Request(url, options, defaultOptions); 48 | let promise: CancelableRequest | undefined; 49 | 50 | const lastHandler = (normalized: Options): GotReturn => { 51 | // Note: `options` is `undefined` when `new Options(...)` fails 52 | request.options = normalized; 53 | request._noPipe = !normalized?.isStream; 54 | void request.flush(); 55 | 56 | if (normalized?.isStream) { 57 | return request; 58 | } 59 | 60 | promise ||= asPromise(request); 61 | 62 | return promise; 63 | }; 64 | 65 | let iteration = 0; 66 | const iterateHandlers = (newOptions: Options): GotReturn => { 67 | const handler = defaults.handlers[iteration++] ?? lastHandler; 68 | 69 | const result = handler(newOptions, iterateHandlers) as GotReturn; 70 | 71 | if (is.promise(result) && !request.options?.isStream) { 72 | promise ||= asPromise(request); 73 | 74 | if (result !== promise) { 75 | const descriptors = Object.getOwnPropertyDescriptors(promise); 76 | 77 | for (const key in descriptors) { 78 | if (key in result) { 79 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 80 | delete descriptors[key]; 81 | } 82 | } 83 | 84 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 85 | Object.defineProperties(result, descriptors); 86 | 87 | result.cancel = promise.cancel; 88 | } 89 | } 90 | 91 | return result; 92 | }; 93 | 94 | return iterateHandlers(request.options); 95 | }) as Got; 96 | 97 | got.extend = (...instancesOrOptions) => { 98 | const options = new Options(undefined, undefined, defaults.options); 99 | const handlers = [...defaults.handlers]; 100 | 101 | let mutableDefaults: boolean | undefined; 102 | 103 | for (const value of instancesOrOptions) { 104 | if (isGotInstance(value)) { 105 | options.merge(value.defaults.options); 106 | handlers.push(...value.defaults.handlers); 107 | mutableDefaults = value.defaults.mutableDefaults; 108 | } else { 109 | options.merge(value); 110 | 111 | if (value.handlers) { 112 | handlers.push(...value.handlers); 113 | } 114 | 115 | mutableDefaults = value.mutableDefaults; 116 | } 117 | } 118 | 119 | return create({ 120 | options, 121 | handlers, 122 | mutableDefaults: Boolean(mutableDefaults), 123 | }); 124 | }; 125 | 126 | // Pagination 127 | const paginateEach = (async function * (url: string | URL, options?: OptionsWithPagination): AsyncIterableIterator { 128 | let normalizedOptions = new Options(url, options as OptionsInit, defaults.options); 129 | normalizedOptions.resolveBodyOnly = false; 130 | 131 | const {pagination} = normalizedOptions; 132 | 133 | assert.function(pagination.transform); 134 | assert.function(pagination.shouldContinue); 135 | assert.function(pagination.filter); 136 | assert.function(pagination.paginate); 137 | assert.number(pagination.countLimit); 138 | assert.number(pagination.requestLimit); 139 | assert.number(pagination.backoff); 140 | 141 | const allItems: T[] = []; 142 | let {countLimit} = pagination; 143 | 144 | let numberOfRequests = 0; 145 | while (numberOfRequests < pagination.requestLimit) { 146 | if (numberOfRequests !== 0) { 147 | // eslint-disable-next-line no-await-in-loop 148 | await delay(pagination.backoff); 149 | } 150 | 151 | // eslint-disable-next-line no-await-in-loop 152 | const response = (await got(undefined, undefined, normalizedOptions)) as Response; 153 | 154 | // eslint-disable-next-line no-await-in-loop 155 | const parsed: unknown[] = await pagination.transform(response); 156 | const currentItems: T[] = []; 157 | 158 | assert.array(parsed); 159 | 160 | for (const item of parsed) { 161 | if (pagination.filter({item, currentItems, allItems})) { 162 | if (!pagination.shouldContinue({item, currentItems, allItems})) { 163 | return; 164 | } 165 | 166 | yield item as T; 167 | 168 | if (pagination.stackAllItems) { 169 | allItems.push(item as T); 170 | } 171 | 172 | currentItems.push(item as T); 173 | 174 | if (--countLimit <= 0) { 175 | return; 176 | } 177 | } 178 | } 179 | 180 | const optionsToMerge = pagination.paginate({ 181 | response, 182 | currentItems, 183 | allItems, 184 | }); 185 | 186 | if (optionsToMerge === false) { 187 | return; 188 | } 189 | 190 | if (optionsToMerge === response.request.options) { 191 | normalizedOptions = response.request.options; 192 | } else { 193 | normalizedOptions.merge(optionsToMerge); 194 | 195 | assert.any([is.urlInstance, is.undefined], optionsToMerge.url); 196 | 197 | if (optionsToMerge.url !== undefined) { 198 | normalizedOptions.prefixUrl = ''; 199 | normalizedOptions.url = optionsToMerge.url; 200 | } 201 | } 202 | 203 | numberOfRequests++; 204 | } 205 | }); 206 | 207 | got.paginate = paginateEach as GotPaginate; 208 | 209 | got.paginate.all = (async (url: string | URL, options?: OptionsWithPagination) => { 210 | const results: T[] = []; 211 | 212 | for await (const item of paginateEach(url, options)) { 213 | results.push(item); 214 | } 215 | 216 | return results; 217 | }) as GotPaginate['all']; 218 | 219 | // For those who like very descriptive names 220 | got.paginate.each = paginateEach as GotPaginate['each']; 221 | 222 | // Stream API 223 | got.stream = ((url: string | URL, options?: StreamOptions) => got(url, {...options, isStream: true})) as GotStream; 224 | 225 | // Shortcuts 226 | for (const method of aliases) { 227 | got[method] = ((url: string | URL, options?: Options): GotReturn => got(url, {...options, method})) as GotRequestFunction; 228 | 229 | got.stream[method] = ((url: string | URL, options?: StreamOptions) => got(url, {...options, method, isStream: true})) as GotStream; 230 | } 231 | 232 | if (!defaults.mutableDefaults) { 233 | Object.freeze(defaults.handlers); 234 | defaults.options.freeze(); 235 | } 236 | 237 | Object.defineProperty(got, 'defaults', { 238 | value: defaults, 239 | writable: false, 240 | configurable: false, 241 | enumerable: true, 242 | }); 243 | 244 | return got; 245 | }; 246 | 247 | export default create; 248 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | import create from './create.js'; 2 | import type {InstanceDefaults} from './types.js'; 3 | import Options from './core/options.js'; 4 | 5 | const defaults: InstanceDefaults = { 6 | options: new Options(), 7 | handlers: [], 8 | mutableDefaults: false, 9 | }; 10 | 11 | const got = create(defaults); 12 | 13 | export default got; 14 | 15 | // TODO: Remove this in the next major version. 16 | export {got}; 17 | 18 | export {default as Options} from './core/options.js'; 19 | export * from './core/options.js'; 20 | export * from './core/response.js'; 21 | export type {default as Request} from './core/index.js'; 22 | export * from './core/index.js'; 23 | export * from './core/errors.js'; 24 | export type {Delays} from './core/timed-out.js'; 25 | export {default as calculateRetryDelay} from './core/calculate-retry-delay.js'; 26 | export * from './as-promise/types.js'; 27 | export * from './types.js'; 28 | export {default as create} from './create.js'; 29 | export {default as parseLinkHeader} from './core/parse-link-header.js'; 30 | -------------------------------------------------------------------------------- /test/agent.ts: -------------------------------------------------------------------------------- 1 | import {Agent as HttpAgent} from 'node:http'; 2 | import {Agent as HttpsAgent} from 'node:https'; 3 | import test from 'ava'; 4 | import sinon from 'sinon'; 5 | import type {Constructor} from 'type-fest'; 6 | import withServer, {withHttpsServer} from './helpers/with-server.js'; 7 | 8 | const createAgentSpy = (AgentClass: Constructor): {agent: T; spy: sinon.SinonSpy} => { 9 | const agent: T = new AgentClass({keepAlive: true}); 10 | // eslint-disable-next-line import/no-named-as-default-member 11 | const spy = sinon.spy(agent, 'addRequest' as any); 12 | return {agent, spy}; 13 | }; 14 | 15 | test('non-object agent option works with http', withServer, async (t, server, got) => { 16 | server.get('/', (_request, response) => { 17 | response.end('ok'); 18 | }); 19 | 20 | const {agent, spy} = createAgentSpy(HttpAgent); 21 | 22 | t.truthy((await got({ 23 | https: { 24 | rejectUnauthorized: false, 25 | }, 26 | agent: { 27 | http: agent, 28 | }, 29 | })).body); 30 | t.true(spy.calledOnce); 31 | 32 | // Make sure to close all open sockets 33 | agent.destroy(); 34 | }); 35 | 36 | test('non-object agent option works with https', withHttpsServer(), async (t, server, got) => { 37 | server.get('/', (_request, response) => { 38 | response.end('ok'); 39 | }); 40 | 41 | const {agent, spy} = createAgentSpy(HttpsAgent); 42 | 43 | t.truthy((await got({ 44 | https: { 45 | rejectUnauthorized: false, 46 | }, 47 | agent: { 48 | https: agent, 49 | }, 50 | })).body); 51 | t.true(spy.calledOnce); 52 | 53 | // Make sure to close all open sockets 54 | agent.destroy(); 55 | }); 56 | 57 | test('redirects from http to https work with an agent object', withServer, async (t, serverHttp) => { 58 | await withHttpsServer().exec(t, async (t, serverHttps, got) => { 59 | serverHttp.get('/', (_request, response) => { 60 | response.end('http'); 61 | }); 62 | 63 | serverHttps.get('/', (_request, response) => { 64 | response.end('https'); 65 | }); 66 | 67 | serverHttp.get('/httpToHttps', (_request, response) => { 68 | response.writeHead(302, { 69 | location: serverHttps.url, 70 | }); 71 | response.end(); 72 | }); 73 | 74 | const {agent: httpAgent, spy: httpSpy} = createAgentSpy(HttpAgent); 75 | const {agent: httpsAgent, spy: httpsSpy} = createAgentSpy(HttpsAgent); 76 | 77 | t.truthy((await got('httpToHttps', { 78 | prefixUrl: serverHttp.url, 79 | agent: { 80 | http: httpAgent, 81 | https: httpsAgent, 82 | }, 83 | })).body); 84 | t.true(httpSpy.calledOnce); 85 | t.true(httpsSpy.calledOnce); 86 | 87 | // Make sure to close all open sockets 88 | httpAgent.destroy(); 89 | httpsAgent.destroy(); 90 | }); 91 | }); 92 | 93 | test('redirects from https to http work with an agent object', withHttpsServer(), async (t, serverHttps, got) => { 94 | await withServer.exec(t, async (t, serverHttp) => { 95 | serverHttp.get('/', (_request, response) => { 96 | response.end('http'); 97 | }); 98 | 99 | serverHttps.get('/', (_request, response) => { 100 | response.end('https'); 101 | }); 102 | 103 | serverHttps.get('/httpsToHttp', (_request, response) => { 104 | response.writeHead(302, { 105 | location: serverHttp.url, 106 | }); 107 | response.end(); 108 | }); 109 | 110 | const {agent: httpAgent, spy: httpSpy} = createAgentSpy(HttpAgent); 111 | const {agent: httpsAgent, spy: httpsSpy} = createAgentSpy(HttpsAgent); 112 | 113 | t.truthy((await got('httpsToHttp', { 114 | prefixUrl: serverHttps.url, 115 | agent: { 116 | http: httpAgent, 117 | https: httpsAgent, 118 | }, 119 | })).body); 120 | t.true(httpSpy.calledOnce); 121 | t.true(httpsSpy.calledOnce); 122 | 123 | // Make sure to close all open sockets 124 | httpAgent.destroy(); 125 | httpsAgent.destroy(); 126 | }); 127 | }); 128 | 129 | test('socket connect listener cleaned up after request', withHttpsServer(), async (t, server, got) => { 130 | server.get('/', (_request, response) => { 131 | response.end('ok'); 132 | }); 133 | 134 | const {agent} = createAgentSpy(HttpsAgent); 135 | 136 | // Make sure there are no memory leaks when reusing keep-alive sockets 137 | for (let index = 0; index < 20; index++) { 138 | // eslint-disable-next-line no-await-in-loop 139 | await got({ 140 | agent: { 141 | https: agent, 142 | }, 143 | }); 144 | } 145 | 146 | for (const value of Object.values(agent.freeSockets)) { 147 | if (!value) { 148 | continue; 149 | } 150 | 151 | for (const sock of value) { 152 | t.is(sock.listenerCount('connect'), 0); 153 | } 154 | } 155 | 156 | // Make sure to close all open sockets 157 | agent.destroy(); 158 | }); 159 | 160 | test('no socket hung up regression', withServer, async (t, server, got) => { 161 | const agent = new HttpAgent({keepAlive: true}); 162 | const token = 'helloworld'; 163 | 164 | server.get('/', (request, response) => { 165 | if (request.headers.token !== token) { 166 | response.statusCode = 401; 167 | response.end(); 168 | return; 169 | } 170 | 171 | response.end('ok'); 172 | }); 173 | 174 | const {body} = await got({ 175 | agent: { 176 | http: agent, 177 | }, 178 | hooks: { 179 | afterResponse: [ 180 | async (response, retryWithMergedOptions) => { 181 | // Force clean-up 182 | response.socket?.destroy(); 183 | 184 | // Unauthorized 185 | if (response.statusCode === 401) { 186 | return retryWithMergedOptions({ 187 | headers: { 188 | token, 189 | }, 190 | }); 191 | } 192 | 193 | // No changes otherwise 194 | return response; 195 | }, 196 | ], 197 | }, 198 | // Disable automatic retries, manual retries are allowed 199 | retry: { 200 | limit: 0, 201 | }, 202 | }); 203 | 204 | t.is(body, 'ok'); 205 | 206 | agent.destroy(); 207 | }); 208 | 209 | test('accept undefined agent', withServer, async (t, server, got) => { 210 | server.get('/', (_request, response) => { 211 | response.end('ok'); 212 | }); 213 | 214 | const undefinedAgent = undefined; 215 | t.truthy((await got({ 216 | https: { 217 | rejectUnauthorized: false, 218 | }, 219 | agent: undefinedAgent, 220 | })).body); 221 | }); 222 | -------------------------------------------------------------------------------- /test/cancel.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {EventEmitter} from 'node:events'; 3 | import {Readable as ReadableStream} from 'node:stream'; 4 | import {pipeline as streamPipeline} from 'node:stream/promises'; 5 | import test from 'ava'; 6 | import delay from 'delay'; 7 | import {pEvent} from 'p-event'; 8 | import getStream from 'get-stream'; 9 | import type {Handler} from 'express'; 10 | import got, {CancelError, TimeoutError, RequestError} from '../source/index.js'; 11 | import slowDataStream from './helpers/slow-data-stream.js'; 12 | import type {GlobalClock} from './helpers/types.js'; 13 | import type {ExtendedHttpTestServer} from './helpers/create-http-test-server.js'; 14 | import withServer, {withServerAndFakeTimers} from './helpers/with-server.js'; 15 | 16 | const prepareServer = (server: ExtendedHttpTestServer, clock: GlobalClock): {emitter: EventEmitter; promise: Promise} => { 17 | const emitter = new EventEmitter(); 18 | 19 | const promise = new Promise((resolve, reject) => { 20 | server.all('/abort', async (request, response) => { 21 | emitter.emit('connection'); 22 | 23 | request.once('aborted', resolve); 24 | response.once('finish', reject.bind(null, new Error('Request finished instead of aborting.'))); 25 | 26 | try { 27 | await pEvent(request, 'end'); 28 | } catch { 29 | // Node.js 15.0.0 throws AND emits `aborted` 30 | } 31 | 32 | response.end(); 33 | }); 34 | 35 | server.get('/redirect', (_request, response) => { 36 | response.writeHead(302, { 37 | location: `${server.url}/abort`, 38 | }); 39 | response.end(); 40 | 41 | emitter.emit('sentRedirect'); 42 | 43 | clock.tick(3000); 44 | resolve(); 45 | }); 46 | }); 47 | 48 | return {emitter, promise}; 49 | }; 50 | 51 | const downloadHandler = (clock?: GlobalClock): Handler => (_request, response) => { 52 | response.writeHead(200, { 53 | 'transfer-encoding': 'chunked', 54 | }); 55 | 56 | response.flushHeaders(); 57 | 58 | (async () => { 59 | try { 60 | await streamPipeline( 61 | slowDataStream(clock), 62 | response, 63 | ); 64 | } catch {} 65 | 66 | response.end(); 67 | })(); 68 | }; 69 | 70 | test.serial('does not retry after cancelation', withServerAndFakeTimers, async (t, server, got, clock) => { 71 | const {emitter, promise} = prepareServer(server, clock); 72 | 73 | const gotPromise = got('redirect', { 74 | retry: { 75 | calculateDelay() { 76 | t.fail('Makes a new try after cancelation'); 77 | return 0; 78 | }, 79 | }, 80 | }); 81 | 82 | emitter.once('sentRedirect', () => { 83 | gotPromise.cancel(); 84 | }); 85 | 86 | await t.throwsAsync(gotPromise, { 87 | instanceOf: CancelError, 88 | code: 'ERR_CANCELED', 89 | }); 90 | await t.notThrowsAsync(promise, 'Request finished instead of aborting.'); 91 | }); 92 | 93 | test.serial('cleans up request timeouts', withServer, async (t, server, got) => { 94 | server.get('/', () => {}); 95 | 96 | const gotPromise = got({ 97 | timeout: { 98 | request: 10, 99 | }, 100 | retry: { 101 | calculateDelay({computedValue}) { 102 | process.nextTick(() => { 103 | gotPromise.cancel(); 104 | }); 105 | 106 | if (computedValue) { 107 | return 20; 108 | } 109 | 110 | return 0; 111 | }, 112 | limit: 1, 113 | }, 114 | }); 115 | 116 | await t.throwsAsync(gotPromise, { 117 | instanceOf: CancelError, 118 | code: 'ERR_CANCELED', 119 | }); 120 | 121 | // Wait for unhandled errors 122 | await delay(40); 123 | }); 124 | 125 | test.serial('cancels in-progress request', withServerAndFakeTimers, async (t, server, got, clock) => { 126 | const {emitter, promise} = prepareServer(server, clock); 127 | 128 | const body = new ReadableStream({ 129 | read() {}, 130 | }); 131 | body.push('1'); 132 | 133 | const gotPromise = got.post('abort', {body}); 134 | 135 | // Wait for the connection to be established before canceling 136 | emitter.once('connection', () => { 137 | gotPromise.cancel(); 138 | body.push(null); 139 | }); 140 | 141 | await t.throwsAsync(gotPromise, { 142 | instanceOf: CancelError, 143 | code: 'ERR_CANCELED', 144 | }); 145 | await t.notThrowsAsync(promise, 'Request finished instead of aborting.'); 146 | }); 147 | 148 | test.serial('cancels in-progress request with timeout', withServerAndFakeTimers, async (t, server, got, clock) => { 149 | const {emitter, promise} = prepareServer(server, clock); 150 | 151 | const body = new ReadableStream({ 152 | read() {}, 153 | }); 154 | body.push('1'); 155 | 156 | const gotPromise = got.post('abort', {body, timeout: {request: 10_000}}); 157 | 158 | // Wait for the connection to be established before canceling 159 | emitter.once('connection', () => { 160 | gotPromise.cancel(); 161 | body.push(null); 162 | }); 163 | 164 | await t.throwsAsync(gotPromise, { 165 | instanceOf: CancelError, 166 | code: 'ERR_CANCELED', 167 | }); 168 | await t.notThrowsAsync(promise, 'Request finished instead of aborting.'); 169 | }); 170 | 171 | test.serial('cancel immediately', withServerAndFakeTimers, async (t, server, got, clock) => { 172 | const promise = new Promise((resolve, reject) => { 173 | // We won't get an abort or even a connection 174 | // We assume no request within 1000ms equals a (client side) aborted request 175 | server.get('/abort', (_request, response) => { 176 | response.once('finish', reject.bind(global, new Error('Request finished instead of aborting.'))); 177 | response.end(); 178 | }); 179 | 180 | clock.tick(1000); 181 | resolve(); 182 | }); 183 | 184 | const gotPromise = got('abort'); 185 | gotPromise.cancel(); 186 | 187 | await t.throwsAsync(gotPromise, { 188 | instanceOf: CancelError, 189 | code: 'ERR_CANCELED', 190 | }); 191 | await t.notThrowsAsync(promise, 'Request finished instead of aborting.'); 192 | }); 193 | 194 | test('recover from cancelation using cancelable promise attribute', async t => { 195 | // Canceled before connection started 196 | const p = got('http://example.com'); 197 | const recover = p.catch((error: Error) => { 198 | if (p.isCanceled) { 199 | return; 200 | } 201 | 202 | throw error; 203 | }); 204 | 205 | p.cancel(); 206 | 207 | await t.notThrowsAsync(recover); 208 | }); 209 | 210 | test('recover from cancellation using error instance', async t => { 211 | // Canceled before connection started 212 | const p = got('http://example.com'); 213 | const recover = p.catch((error: Error) => { 214 | if (error instanceof CancelError) { 215 | return; 216 | } 217 | 218 | throw error; 219 | }); 220 | 221 | p.cancel(); 222 | 223 | await t.notThrowsAsync(recover); 224 | }); 225 | 226 | test.serial('throws on incomplete (canceled) response - promise', withServerAndFakeTimers, async (t, server, got, clock) => { 227 | server.get('/', downloadHandler(clock)); 228 | 229 | await t.throwsAsync( 230 | got({ 231 | timeout: {request: 500}, 232 | retry: { 233 | limit: 0, 234 | }, 235 | }), 236 | {instanceOf: TimeoutError}, 237 | ); 238 | }); 239 | 240 | test.serial('throws on incomplete (canceled) response - promise #2', withServerAndFakeTimers, async (t, server, got, clock) => { 241 | server.get('/', downloadHandler(clock)); 242 | 243 | const promise = got(''); 244 | 245 | setTimeout(() => { 246 | promise.cancel(); 247 | }, 500); 248 | 249 | await t.throwsAsync(promise, { 250 | instanceOf: CancelError, 251 | code: 'ERR_CANCELED', 252 | }); 253 | }); 254 | 255 | test.serial('throws on incomplete (canceled) response - stream', withServerAndFakeTimers, async (t, server, got, clock) => { 256 | server.get('/', downloadHandler(clock)); 257 | 258 | const errorString = 'Foobar'; 259 | 260 | const stream = got.stream('').on('response', () => { 261 | clock.tick(500); 262 | stream.destroy(new Error(errorString)); 263 | }); 264 | 265 | await t.throwsAsync(getStream(stream), { 266 | instanceOf: RequestError, 267 | message: errorString, 268 | }); 269 | }); 270 | 271 | test('throws when canceling cached request', withServer, async (t, server, got) => { 272 | server.get('/', (_request, response) => { 273 | response.setHeader('Cache-Control', 'public, max-age=60'); 274 | response.end(Date.now().toString()); 275 | }); 276 | 277 | const cache = new Map(); 278 | await got({cache}); 279 | 280 | const promise = got({cache}); 281 | promise.cancel(); 282 | 283 | await t.throwsAsync(promise, { 284 | instanceOf: CancelError, 285 | code: 'ERR_CANCELED', 286 | }); 287 | }); 288 | -------------------------------------------------------------------------------- /test/cookies.ts: -------------------------------------------------------------------------------- 1 | import net from 'node:net'; 2 | import test from 'ava'; 3 | import toughCookie from 'tough-cookie'; 4 | import delay from 'delay'; 5 | import {got, RequestError} from '../source/index.js'; 6 | import withServer from './helpers/with-server.js'; 7 | 8 | test('reads a cookie', withServer, async (t, server, got) => { 9 | server.get('/', (_request, response) => { 10 | response.setHeader('set-cookie', 'hello=world'); 11 | response.end(); 12 | }); 13 | 14 | const cookieJar = new toughCookie.CookieJar(); 15 | 16 | await got({cookieJar}); 17 | 18 | const cookie = cookieJar.getCookiesSync(server.url)[0]; 19 | t.is(cookie?.key, 'hello'); 20 | t.is(cookie?.value, 'world'); 21 | }); 22 | 23 | test('reads multiple cookies', withServer, async (t, server, got) => { 24 | server.get('/', (_request, response) => { 25 | response.setHeader('set-cookie', ['hello=world', 'foo=bar']); 26 | response.end(); 27 | }); 28 | 29 | const cookieJar = new toughCookie.CookieJar(); 30 | 31 | await got({cookieJar}); 32 | 33 | const [cookieA, cookieB] = cookieJar.getCookiesSync(server.url); 34 | t.is(cookieA!.key, 'hello'); 35 | t.is(cookieA!.value, 'world'); 36 | t.is(cookieB!.key, 'foo'); 37 | t.is(cookieB!.value, 'bar'); 38 | }); 39 | 40 | test('cookies doesn\'t break on redirects', withServer, async (t, server, got) => { 41 | server.get('/redirect', (_request, response) => { 42 | response.setHeader('set-cookie', ['hello=world', 'foo=bar']); 43 | response.setHeader('location', '/'); 44 | response.statusCode = 302; 45 | response.end(); 46 | }); 47 | 48 | server.get('/', (request, response) => { 49 | response.end(request.headers.cookie ?? ''); 50 | }); 51 | 52 | const cookieJar = new toughCookie.CookieJar(); 53 | 54 | const {body} = await got('redirect', {cookieJar}); 55 | t.is(body, 'hello=world; foo=bar'); 56 | }); 57 | 58 | test('throws on invalid cookies', withServer, async (t, server, got) => { 59 | server.get('/', (_request, response) => { 60 | response.setHeader('set-cookie', 'invalid cookie; domain=localhost'); 61 | response.end(); 62 | }); 63 | 64 | const cookieJar = new toughCookie.CookieJar(); 65 | 66 | await t.throwsAsync(got({cookieJar}), { 67 | instanceOf: RequestError, 68 | message: 'Cookie failed to parse', 69 | }); 70 | }); 71 | 72 | test('does not throw on invalid cookies when options.ignoreInvalidCookies is set', withServer, async (t, server, got) => { 73 | server.get('/', (_request, response) => { 74 | response.setHeader('set-cookie', 'invalid cookie; domain=localhost'); 75 | response.end(); 76 | }); 77 | 78 | const cookieJar = new toughCookie.CookieJar(); 79 | 80 | await got({ 81 | cookieJar, 82 | ignoreInvalidCookies: true, 83 | }); 84 | 85 | const cookies = cookieJar.getCookiesSync(server.url); 86 | t.is(cookies.length, 0); 87 | }); 88 | 89 | test('catches store errors', async t => { 90 | const error = 'Some error'; 91 | const cookieJar = new toughCookie.CookieJar({ 92 | findCookies(_, __, ___, callback) { 93 | callback(new Error(error), []); 94 | }, 95 | findCookie() {}, 96 | getAllCookies() {}, 97 | putCookie() {}, 98 | removeCookies() {}, 99 | removeCookie() {}, 100 | updateCookie() {}, 101 | synchronous: false, 102 | }); 103 | 104 | await t.throwsAsync(got('https://example.com', {cookieJar}), { 105 | instanceOf: RequestError, 106 | message: error, 107 | }); 108 | }); 109 | 110 | test('overrides options.headers.cookie', withServer, async (t, server, got) => { 111 | server.get('/redirect', (_request, response) => { 112 | response.setHeader('set-cookie', ['hello=world', 'foo=bar']); 113 | response.setHeader('location', '/'); 114 | response.statusCode = 302; 115 | response.end(); 116 | }); 117 | 118 | server.get('/', (request, response) => { 119 | response.end(request.headers.cookie ?? ''); 120 | }); 121 | 122 | const cookieJar = new toughCookie.CookieJar(); 123 | const {body} = await got('redirect', { 124 | cookieJar, 125 | headers: { 126 | cookie: 'a=b', 127 | }, 128 | }); 129 | t.is(body, 'hello=world; foo=bar'); 130 | }); 131 | 132 | test('no unhandled errors', async t => { 133 | const server = net.createServer(connection => { 134 | connection.end('blah'); 135 | }).listen(0); 136 | 137 | const message = 'snap!'; 138 | 139 | const options = { 140 | cookieJar: { 141 | async setCookie(_rawCookie: string, _url: string) {}, 142 | async getCookieString(_url: string) { 143 | throw new Error(message); 144 | }, 145 | }, 146 | }; 147 | 148 | await t.throwsAsync(got(`http://127.0.0.1:${(server.address() as net.AddressInfo).port}`, options), { 149 | instanceOf: RequestError, 150 | message, 151 | }); 152 | await delay(500); 153 | t.pass(); 154 | 155 | server.close(); 156 | }); 157 | 158 | test('accepts custom `cookieJar` object', withServer, async (t, server, got) => { 159 | server.get('/', (request, response) => { 160 | response.setHeader('set-cookie', ['hello=world']); 161 | response.end(request.headers.cookie); 162 | }); 163 | 164 | const cookies: Record = {}; 165 | const cookieJar = { 166 | async getCookieString(url: string) { 167 | t.is(typeof url, 'string'); 168 | return cookies[url] ?? ''; 169 | }, 170 | 171 | async setCookie(rawCookie: string, url: string) { 172 | cookies[url] = rawCookie; 173 | }, 174 | }; 175 | 176 | const first = await got('', {cookieJar}); 177 | const second = await got('', {cookieJar}); 178 | 179 | t.is(first.body, ''); 180 | t.is(second.body, 'hello=world'); 181 | }); 182 | 183 | test('throws on invalid `options.cookieJar.setCookie`', async t => { 184 | await t.throwsAsync(got('https://example.com', { 185 | cookieJar: { 186 | // @ts-expect-error Error tests 187 | setCookie: 123, 188 | }, 189 | }), { 190 | instanceOf: RequestError, 191 | message: 'Expected value which is `Function`, received value of type `number`.', 192 | }); 193 | }); 194 | 195 | test('throws on invalid `options.cookieJar.getCookieString`', async t => { 196 | await t.throwsAsync(got('https://example.com', { 197 | cookieJar: { 198 | async setCookie() {}, 199 | // @ts-expect-error Error tests 200 | getCookieString: 123, 201 | }, 202 | }), { 203 | instanceOf: RequestError, 204 | message: 'Expected value which is `Function`, received value of type `number`.', 205 | }); 206 | }); 207 | 208 | test('cookies are cleared when redirecting to a different hostname (no cookieJar)', withServer, async (t, server, got) => { 209 | server.get('/', (_request, response) => { 210 | response.writeHead(302, { 211 | location: 'https://httpbin.org/anything', 212 | }); 213 | response.end(); 214 | }); 215 | 216 | const {headers} = await got('', { 217 | headers: { 218 | cookie: 'foo=bar', 219 | 'user-agent': 'custom', 220 | }, 221 | }).json<{headers: Record}>(); 222 | t.is(headers.Cookie, undefined); 223 | t.is(headers['User-Agent'], 'custom'); 224 | }); 225 | -------------------------------------------------------------------------------- /test/encoding.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'node:buffer'; 2 | import test from 'ava'; 3 | import withServer from './helpers/with-server.js'; 4 | 5 | test('encoding works with json', withServer, async (t, server, got) => { 6 | const json = {data: 'é'}; 7 | 8 | server.get('/', (_request, response) => { 9 | response.set('Content-Type', 'application-json'); 10 | response.send(Buffer.from(JSON.stringify(json), 'latin1')); 11 | }); 12 | 13 | const response = await got('', { 14 | encoding: 'latin1', 15 | responseType: 'json', 16 | }); 17 | 18 | t.deepEqual(response.body, json); 19 | }); 20 | -------------------------------------------------------------------------------- /test/extend.types.ts: -------------------------------------------------------------------------------- 1 | import type {Buffer} from 'node:buffer'; 2 | import {expectTypeOf} from 'expect-type'; 3 | import got, {type CancelableRequest, type Response} from '../source/index.js'; 4 | import {type Got, type MergeExtendsConfig, type ExtractExtendOptions} from '../source/types.js'; 5 | 6 | // Ensure we properly extract the `extend` options from a Got instance which is used in MergeExtendsConfig generic 7 | expectTypeOf>>().toEqualTypeOf<{resolveBodyOnly: false}>(); 8 | expectTypeOf>>().toEqualTypeOf<{resolveBodyOnly: true}>(); 9 | expectTypeOf>().toEqualTypeOf<{resolveBodyOnly: false}>(); 10 | expectTypeOf>().toEqualTypeOf<{resolveBodyOnly: true}>(); 11 | 12 | // 13 | // Tests for MergeExtendsConfig - which merges the potential arguments of the `got.extend` method 14 | // 15 | // MergeExtendsConfig works with a single value 16 | expectTypeOf>().toEqualTypeOf<{resolveBodyOnly: false}>(); 17 | expectTypeOf>().toEqualTypeOf<{resolveBodyOnly: true}>(); 18 | expectTypeOf]>>().toEqualTypeOf<{resolveBodyOnly: false}>(); 19 | expectTypeOf]>>().toEqualTypeOf<{resolveBodyOnly: true}>(); 20 | 21 | // MergeExtendsConfig merges multiple ExtendOptions 22 | expectTypeOf>().toEqualTypeOf<{resolveBodyOnly: true}>(); 23 | expectTypeOf>().toEqualTypeOf<{resolveBodyOnly: false}>(); 24 | 25 | // MergeExtendsConfig merges multiple Got instances 26 | expectTypeOf, Got<{resolveBodyOnly: true}>]>>().toEqualTypeOf<{resolveBodyOnly: true}>(); 27 | expectTypeOf, Got<{resolveBodyOnly: false}>]>>().toEqualTypeOf<{resolveBodyOnly: false}>(); 28 | 29 | // MergeExtendsConfig merges multiple Got instances and ExtendOptions with Got first argument 30 | expectTypeOf, {resolveBodyOnly: true}]>>().toEqualTypeOf<{resolveBodyOnly: true}>(); 31 | expectTypeOf, {resolveBodyOnly: false}]>>().toEqualTypeOf<{resolveBodyOnly: false}>(); 32 | 33 | // MergeExtendsConfig merges multiple Got instances and ExtendOptions with ExtendOptions first argument 34 | expectTypeOf]>>().toEqualTypeOf<{resolveBodyOnly: false}>(); 35 | expectTypeOf]>>().toEqualTypeOf<{resolveBodyOnly: true}>(); 36 | 37 | // 38 | // Test the implementation of got.extend types 39 | // 40 | expectTypeOf(got.extend({resolveBodyOnly: false})).toEqualTypeOf>(); 41 | expectTypeOf(got.extend({resolveBodyOnly: true})).toEqualTypeOf>(); 42 | expectTypeOf(got.extend(got.extend({resolveBodyOnly: true}))).toEqualTypeOf>(); 43 | expectTypeOf(got.extend(got.extend({resolveBodyOnly: false}))).toEqualTypeOf>(); 44 | expectTypeOf(got.extend(got.extend({resolveBodyOnly: true}), {resolveBodyOnly: false})).toEqualTypeOf>(); 45 | expectTypeOf(got.extend(got.extend({resolveBodyOnly: false}), {resolveBodyOnly: true})).toEqualTypeOf>(); 46 | expectTypeOf(got.extend({resolveBodyOnly: true}, got.extend({resolveBodyOnly: false}))).toEqualTypeOf>(); 47 | expectTypeOf(got.extend({resolveBodyOnly: false}, got.extend({resolveBodyOnly: true}))).toEqualTypeOf>(); 48 | 49 | // 50 | // Test that created instances enable the correct return types for the request functions 51 | // 52 | const gotWrapped = got.extend({}); 53 | 54 | // The following tests would apply to all of the method signatures (get, post, put, delete, etc...), but we only test the base function for brevity 55 | 56 | // Test the default instance 57 | expectTypeOf(gotWrapped('https://example.com')).toEqualTypeOf>>(); 58 | expectTypeOf(gotWrapped<{test: 'test'}>('https://example.com')).toEqualTypeOf>>(); 59 | expectTypeOf(gotWrapped('https://example.com', {responseType: 'buffer'})).toEqualTypeOf>>(); 60 | 61 | // Test the default instance can be overridden at the request function level 62 | expectTypeOf(gotWrapped('https://example.com', {resolveBodyOnly: true})).toEqualTypeOf>(); 63 | expectTypeOf(gotWrapped<{test: 'test'}>('https://example.com', {resolveBodyOnly: true})).toEqualTypeOf>(); 64 | expectTypeOf(gotWrapped('https://example.com', {responseType: 'buffer', resolveBodyOnly: true})).toEqualTypeOf>(); 65 | 66 | const gotBodyOnly = got.extend({resolveBodyOnly: true}); 67 | 68 | // Test the instance with resolveBodyOnly as an extend option 69 | expectTypeOf(gotBodyOnly('https://example.com')).toEqualTypeOf>(); 70 | expectTypeOf(gotBodyOnly<{test: 'test'}>('https://example.com')).toEqualTypeOf>(); 71 | expectTypeOf(gotBodyOnly('https://example.com', {responseType: 'buffer'})).toEqualTypeOf>(); 72 | 73 | // Test the instance with resolveBodyOnly as an extend option can be overridden at the request function level 74 | expectTypeOf(gotBodyOnly('https://example.com', {resolveBodyOnly: false})).toEqualTypeOf>>(); 75 | expectTypeOf(gotBodyOnly<{test: 'test'}>('https://example.com', {resolveBodyOnly: false})).toEqualTypeOf>>(); 76 | expectTypeOf(gotBodyOnly('https://example.com', {responseType: 'buffer', resolveBodyOnly: false})).toEqualTypeOf>>(); 77 | -------------------------------------------------------------------------------- /test/fixtures/ok: -------------------------------------------------------------------------------- 1 | ok 2 | -------------------------------------------------------------------------------- /test/fixtures/stream-content-length: -------------------------------------------------------------------------------- 1 | Unicorns 2 | -------------------------------------------------------------------------------- /test/gzip.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'node:buffer'; 2 | import {promisify} from 'node:util'; 3 | import zlib from 'node:zlib'; 4 | import test from 'ava'; 5 | import getStream from 'get-stream'; 6 | import {ReadError, type HTTPError} from '../source/index.js'; 7 | import withServer from './helpers/with-server.js'; 8 | 9 | const testContent = 'Compressible response content.\n'; 10 | const testContentUncompressed = 'Uncompressed response content.\n'; 11 | 12 | let gzipData: Buffer; 13 | test.before('setup', async () => { 14 | gzipData = await promisify(zlib.gzip)(testContent); 15 | }); 16 | 17 | test('decompress content', withServer, async (t, server, got) => { 18 | server.get('/', (_request, response) => { 19 | response.setHeader('Content-Encoding', 'gzip'); 20 | response.end(gzipData); 21 | }); 22 | 23 | t.is((await got('')).body, testContent); 24 | }); 25 | 26 | test('decompress content on error', withServer, async (t, server, got) => { 27 | server.get('/', (_request, response) => { 28 | response.setHeader('Content-Encoding', 'gzip'); 29 | response.status(404); 30 | response.end(gzipData); 31 | }); 32 | 33 | const error = await t.throwsAsync(got('')); 34 | 35 | t.is(error?.response.body, testContent); 36 | }); 37 | 38 | test('decompress content - stream', withServer, async (t, server, got) => { 39 | server.get('/', (_request, response) => { 40 | response.setHeader('Content-Encoding', 'gzip'); 41 | response.end(gzipData); 42 | }); 43 | 44 | t.is((await getStream(got.stream(''))), testContent); 45 | }); 46 | 47 | test('handles gzip error', withServer, async (t, server, got) => { 48 | server.get('/', (_request, response) => { 49 | response.setHeader('Content-Encoding', 'gzip'); 50 | response.end('Not gzipped content'); 51 | }); 52 | 53 | await t.throwsAsync(got(''), { 54 | name: 'ReadError', 55 | message: 'incorrect header check', 56 | }); 57 | }); 58 | 59 | test('no unhandled `Premature close` error', withServer, async (t, server, got) => { 60 | server.get('/', (_request, response) => { 61 | response.setHeader('Content-Encoding', 'gzip'); 62 | response.write('Not gzipped content'); 63 | }); 64 | 65 | await t.throwsAsync(got(''), { 66 | name: 'ReadError', 67 | // `The server aborted pending request` on Node.js 15 or later. 68 | message: /incorrect header check|The server aborted pending request/, 69 | }); 70 | }); 71 | 72 | test('handles gzip error - stream', withServer, async (t, server, got) => { 73 | server.get('/', (_request, response) => { 74 | response.setHeader('Content-Encoding', 'gzip'); 75 | response.end('Not gzipped content'); 76 | }); 77 | 78 | await t.throwsAsync(getStream(got.stream('')), { 79 | name: 'ReadError', 80 | message: 'incorrect header check', 81 | }); 82 | }); 83 | 84 | test('decompress option opts out of decompressing', withServer, async (t, server, got) => { 85 | server.get('/', (_request, response) => { 86 | response.setHeader('Content-Encoding', 'gzip'); 87 | response.end(gzipData); 88 | }); 89 | 90 | const {body} = await got({decompress: false, responseType: 'buffer'}); 91 | t.is(Buffer.compare(body, gzipData), 0); 92 | }); 93 | 94 | test('decompress option doesn\'t alter encoding of uncompressed responses', withServer, async (t, server, got) => { 95 | server.get('/', (_request, response) => { 96 | response.end(testContentUncompressed); 97 | }); 98 | 99 | const {body} = await got({decompress: false}); 100 | t.is(body, testContentUncompressed); 101 | }); 102 | 103 | test('preserves `headers` property', withServer, async (t, server, got) => { 104 | server.get('/', (_request, response) => { 105 | response.setHeader('Content-Encoding', 'gzip'); 106 | response.end(gzipData); 107 | }); 108 | 109 | t.truthy((await got('')).headers); 110 | }); 111 | 112 | test('does not break HEAD responses', withServer, async (t, server, got) => { 113 | server.get('/', (_request, response) => { 114 | response.end(); 115 | }); 116 | 117 | t.is((await got.head('')).body, ''); 118 | }); 119 | 120 | test('does not ignore missing data', withServer, async (t, server, got) => { 121 | server.get('/', (_request, response) => { 122 | response.setHeader('Content-Encoding', 'gzip'); 123 | response.end(gzipData.slice(0, -1)); 124 | }); 125 | 126 | await t.throwsAsync(got(''), { 127 | instanceOf: ReadError, 128 | message: 'unexpected end of file', 129 | }); 130 | }); 131 | 132 | test('response has `url` and `requestUrl` properties', withServer, async (t, server, got) => { 133 | server.get('/', (_request, response) => { 134 | response.setHeader('Content-Encoding', 'gzip'); 135 | response.end(gzipData); 136 | }); 137 | 138 | const response = await got(''); 139 | t.truthy(response.url); 140 | t.truthy(response.requestUrl); 141 | }); 142 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import got, {HTTPError} from '../source/index.js'; 3 | import withServer from './helpers/with-server.js'; 4 | import invalidUrl from './helpers/invalid-url.js'; 5 | 6 | test('works', withServer, async (t, server) => { 7 | server.get('/', (_request, response) => { 8 | response.end('ok'); 9 | }); 10 | 11 | server.get('/404', (_request, response) => { 12 | response.statusCode = 404; 13 | response.end('not found'); 14 | }); 15 | 16 | const {body} = await got.get(server.url); 17 | t.is(body, 'ok'); 18 | 19 | const error = await t.throwsAsync(got.get(`${server.url}/404`), {instanceOf: HTTPError}); 20 | t.is(error?.response.body, 'not found'); 21 | 22 | const secondError = await t.throwsAsync(got.get('.com', {retry: {limit: 0}})); 23 | invalidUrl(t, secondError!, '.com'); 24 | }); 25 | -------------------------------------------------------------------------------- /test/helpers/create-http-test-server.ts: -------------------------------------------------------------------------------- 1 | import http from 'node:http'; 2 | import type net from 'node:net'; 3 | import express, {type Express, type NextFunction} from 'express'; 4 | import pify from 'pify'; 5 | import bodyParser from 'body-parser'; 6 | 7 | export type HttpServerOptions = { 8 | bodyParser?: NextFunction | false; 9 | }; 10 | 11 | export type ExtendedHttpTestServer = { 12 | http: http.Server; 13 | url: string; 14 | port: number; 15 | hostname: string; 16 | close: () => Promise; 17 | } & Express; 18 | 19 | const createHttpTestServer = async (options: HttpServerOptions = {}): Promise => { 20 | const server = express() as ExtendedHttpTestServer; 21 | server.http = http.createServer(server); 22 | 23 | server.set('etag', false); 24 | 25 | if (options.bodyParser !== false) { 26 | server.use(bodyParser.json({limit: '1mb', type: 'application/json', ...options.bodyParser})); 27 | server.use(bodyParser.text({limit: '1mb', type: 'text/plain', ...options.bodyParser})); 28 | 29 | server.use(bodyParser.urlencoded({ 30 | limit: '1mb', 31 | type: 'application/x-www-form-urlencoded', 32 | extended: true, 33 | ...options.bodyParser, 34 | })); 35 | 36 | server.use(bodyParser.raw({limit: '1mb', type: 'application/octet-stream', ...options.bodyParser})); 37 | } 38 | 39 | await pify(server.http.listen.bind(server.http))(); 40 | 41 | server.port = (server.http.address() as net.AddressInfo).port; 42 | server.url = `http://localhost:${(server.port)}`; 43 | server.hostname = 'localhost'; 44 | 45 | server.close = async () => pify(server.http.close.bind(server.http))(); 46 | 47 | return server; 48 | }; 49 | 50 | export default createHttpTestServer; 51 | -------------------------------------------------------------------------------- /test/helpers/create-https-test-server.ts: -------------------------------------------------------------------------------- 1 | import type {Buffer} from 'node:buffer'; 2 | import https from 'node:https'; 3 | import type net from 'node:net'; 4 | import type {SecureContextOptions} from 'node:tls'; 5 | import express from 'express'; 6 | import pify from 'pify'; 7 | import pem from 'pem'; 8 | import type {CreateCsr, CreateCertificate} from '../types/pem.js'; 9 | 10 | export type HttpsServerOptions = { 11 | commonName?: string; 12 | days?: number; 13 | ciphers?: SecureContextOptions['ciphers']; 14 | honorCipherOrder?: SecureContextOptions['honorCipherOrder']; 15 | minVersion?: SecureContextOptions['minVersion']; 16 | maxVersion?: SecureContextOptions['maxVersion']; 17 | }; 18 | 19 | export type ExtendedHttpsTestServer = { 20 | https: https.Server; 21 | caKey: Buffer; 22 | caCert: Buffer; 23 | url: string; 24 | port: number; 25 | close: () => Promise; 26 | } & express.Express; 27 | 28 | const createHttpsTestServer = async (options: HttpsServerOptions = {}): Promise => { 29 | const createCsr = pify(pem.createCSR as CreateCsr); 30 | const createCertificate = pify(pem.createCertificate as CreateCertificate); 31 | 32 | const caCsrResult = await createCsr({commonName: 'authority'}); 33 | const caResult = await createCertificate({ 34 | csr: caCsrResult.csr, 35 | clientKey: caCsrResult.clientKey, 36 | selfSigned: true, 37 | }); 38 | const caKey = caResult.clientKey; 39 | const caCert = caResult.certificate; 40 | 41 | const serverCsrResult = await createCsr({commonName: options.commonName ?? 'localhost'}); 42 | const serverResult = await createCertificate({ 43 | csr: serverCsrResult.csr, 44 | clientKey: serverCsrResult.clientKey, 45 | serviceKey: caKey, 46 | serviceCertificate: caCert, 47 | days: options.days ?? 365, 48 | }); 49 | const serverKey = serverResult.clientKey; 50 | const serverCert = serverResult.certificate; 51 | 52 | const server = express() as ExtendedHttpsTestServer; 53 | server.https = https.createServer( 54 | { 55 | key: serverKey, 56 | cert: serverCert, 57 | ca: caCert, 58 | requestCert: true, 59 | rejectUnauthorized: false, // This should be checked by the test 60 | ciphers: options.ciphers, 61 | honorCipherOrder: options.honorCipherOrder, 62 | minVersion: options.minVersion, 63 | maxVersion: options.maxVersion, 64 | }, 65 | server, 66 | ); 67 | 68 | server.set('etag', false); 69 | 70 | await pify(server.https.listen.bind(server.https))(); 71 | 72 | server.caKey = caKey as any; 73 | server.caCert = caCert; 74 | server.port = (server.https.address() as net.AddressInfo).port; 75 | server.url = `https://localhost:${(server.port)}`; 76 | 77 | server.close = async () => pify(server.https.close.bind(server.https))(); 78 | 79 | return server; 80 | }; 81 | 82 | export default createHttpsTestServer; 83 | -------------------------------------------------------------------------------- /test/helpers/invalid-url.ts: -------------------------------------------------------------------------------- 1 | 2 | import type {ExecutionContext} from 'ava'; 3 | 4 | export default function invalidUrl(t: ExecutionContext, error: TypeError & NodeJS.ErrnoException, url: string) { 5 | t.is(error.code, 'ERR_INVALID_URL'); 6 | 7 | if (error.message === 'Invalid URL') { 8 | t.is((error as any).input, url); 9 | } else { 10 | t.is(error.message.slice('Invalid URL: '.length), url); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/helpers/slow-data-stream.ts: -------------------------------------------------------------------------------- 1 | import {Readable} from 'node:stream'; 2 | import type {Clock} from '@sinonjs/fake-timers'; 3 | import delay from 'delay'; 4 | 5 | export default function slowDataStream(clock?: Clock): Readable { 6 | let index = 0; 7 | 8 | return new Readable({ 9 | async read() { 10 | if (clock) { 11 | clock.tick(100); 12 | } else { 13 | await delay(100); 14 | } 15 | 16 | if (index++ < 10) { 17 | this.push('data\n'.repeat(100)); 18 | } else { 19 | this.push(null); 20 | } 21 | }, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /test/helpers/types.ts: -------------------------------------------------------------------------------- 1 | import type {Server} from 'node:http'; 2 | // @ts-expect-error Fails to locate ../types/create-test-server/index.d.ts 3 | import type {TestServer} from 'create-test-server'; 4 | 5 | export type ExtendedHttpServer = { 6 | socketPath: string; 7 | } & Server; 8 | 9 | export type ExtendedTestServer = { 10 | hostname: string; 11 | sslHostname: string; 12 | } & TestServer; 13 | 14 | // https://github.com/sinonjs/fake-timers/pull/386 15 | export type InstalledClock = any; 16 | export type GlobalClock = any; 17 | -------------------------------------------------------------------------------- /test/helpers/with-server.ts: -------------------------------------------------------------------------------- 1 | import http from 'node:http'; 2 | import {promisify} from 'node:util'; 3 | import type {ExecutionContext, Macro} from 'ava'; 4 | import is from '@sindresorhus/is'; 5 | import {temporaryFile} from 'tempy'; 6 | import FakeTimers from '@sinonjs/fake-timers'; 7 | import got, {type Got, type ExtendOptions} from '../../source/index.js'; 8 | import createHttpsTestServer, { 9 | type ExtendedHttpsTestServer, 10 | type HttpsServerOptions, 11 | } from './create-https-test-server.js'; 12 | import createHttpTestServer, { 13 | type ExtendedHttpTestServer, 14 | type HttpServerOptions, 15 | } from './create-http-test-server.js'; 16 | import type {ExtendedHttpServer, GlobalClock, InstalledClock} from './types.js'; 17 | 18 | export type RunTestWithServer = (t: ExecutionContext, server: ExtendedHttpTestServer, got: Got, clock: GlobalClock) => Promise | void; 19 | export type RunTestWithHttpsServer = (t: ExecutionContext, server: ExtendedHttpsTestServer, got: Got, fakeTimer?: GlobalClock) => Promise | void; 20 | export type RunTestWithSocket = (t: ExecutionContext, server: ExtendedHttpServer) => Promise | void; 21 | 22 | const generateHook = ({install, options: testServerOptions}: {install?: boolean; options?: HttpServerOptions}): Macro<[RunTestWithServer]> => ({ 23 | async exec(t, run) { 24 | const clock = install ? FakeTimers.install() : FakeTimers.createClock() as GlobalClock; 25 | 26 | // Re-enable body parsing to investigate https://github.com/sindresorhus/got/issues/1186 27 | const server = await createHttpTestServer(is.plainObject(testServerOptions) ? testServerOptions : { 28 | bodyParser: { 29 | type: () => false, 30 | } as any, 31 | }); 32 | 33 | const options: ExtendOptions = { 34 | context: { 35 | avaTest: t.title, 36 | }, 37 | handlers: [ 38 | (options, next) => { 39 | const result = next(options); 40 | 41 | clock.tick(0); 42 | 43 | // @ts-expect-error FIXME: Incompatible union type signatures 44 | result.on('response', () => { 45 | clock.tick(0); 46 | }); 47 | 48 | return result; 49 | }, 50 | ], 51 | }; 52 | 53 | const preparedGot = got.extend({prefixUrl: server.url, ...options}); 54 | 55 | try { 56 | await run(t, server, preparedGot, clock); 57 | } finally { 58 | await server.close(); 59 | } 60 | 61 | if (install) { 62 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 63 | (clock as InstalledClock).uninstall(); 64 | } 65 | }, 66 | }); 67 | 68 | export const withBodyParsingServer = generateHook({install: false, options: {}}); 69 | export default generateHook({install: false}); 70 | 71 | export const withServerAndFakeTimers = generateHook({install: true}); 72 | 73 | const generateHttpsHook = (options?: HttpsServerOptions, installFakeTimer = false): Macro<[RunTestWithHttpsServer]> => ({ 74 | async exec(t, run) { 75 | const fakeTimer = installFakeTimer ? FakeTimers.install() as GlobalClock : undefined; 76 | 77 | const server = await createHttpsTestServer(options); 78 | 79 | const preparedGot = got.extend({ 80 | context: { 81 | avaTest: t.title, 82 | }, 83 | handlers: [ 84 | (options, next) => { 85 | const result = next(options); 86 | 87 | fakeTimer?.tick(0); 88 | 89 | // @ts-expect-error FIXME: Incompatible union type signatures 90 | result.on('response', () => { 91 | fakeTimer?.tick(0); 92 | }); 93 | 94 | return result; 95 | }, 96 | ], 97 | prefixUrl: server.url, 98 | https: { 99 | certificateAuthority: (server as any).caCert, 100 | rejectUnauthorized: true, 101 | }, 102 | }); 103 | 104 | try { 105 | await run(t, server, preparedGot, fakeTimer); 106 | } finally { 107 | await server.close(); 108 | } 109 | 110 | if (installFakeTimer) { 111 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 112 | (fakeTimer as InstalledClock).uninstall(); 113 | } 114 | }, 115 | }); 116 | 117 | export const withHttpsServer = generateHttpsHook; 118 | 119 | // TODO: Remove this when `create-test-server` supports custom listen. 120 | export const withSocketServer: Macro<[RunTestWithSocket]> = { 121 | async exec(t, run) { 122 | const socketPath = temporaryFile({extension: 'socket'}); 123 | 124 | const server = http.createServer((request, response) => { 125 | server.emit(request.url!, request, response); 126 | }) as ExtendedHttpServer; 127 | 128 | server.socketPath = socketPath; 129 | 130 | // @ts-expect-error TypeScript doesn't accept `callback` with no arguments 131 | await promisify(server.listen.bind(server))(socketPath); 132 | 133 | try { 134 | await run(t, server); 135 | } finally { 136 | await promisify(server.close.bind(server))(); 137 | } 138 | }, 139 | }; 140 | -------------------------------------------------------------------------------- /test/merge-instances.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import type {Handler} from 'express'; 3 | import got, {type BeforeRequestHook, type Got, type Headers} from '../source/index.js'; 4 | import withServer from './helpers/with-server.js'; 5 | 6 | const echoHeaders: Handler = (request, response) => { 7 | response.end(JSON.stringify(request.headers)); 8 | }; 9 | 10 | test('merging instances', withServer, async (t, server) => { 11 | server.get('/', echoHeaders); 12 | 13 | const instanceA = got.extend({headers: {unicorn: 'rainbow'}}); 14 | const instanceB = got.extend({prefixUrl: server.url}); 15 | const merged = instanceA.extend(instanceB); 16 | 17 | const headers = await merged('').json(); 18 | t.is(headers.unicorn, 'rainbow'); 19 | t.not(headers['user-agent'], undefined); 20 | }); 21 | 22 | test('merges default handlers & custom handlers', withServer, async (t, server) => { 23 | server.get('/', echoHeaders); 24 | 25 | const instanceA = got.extend({headers: {unicorn: 'rainbow'}}); 26 | const instanceB = got.extend({ 27 | handlers: [ 28 | (options, next) => { 29 | options.headers.cat = 'meow'; 30 | return next(options); 31 | }, 32 | ], 33 | }); 34 | const merged = instanceA.extend(instanceB); 35 | 36 | const headers = await merged(server.url).json(); 37 | t.is(headers.unicorn, 'rainbow'); 38 | t.is(headers.cat, 'meow'); 39 | }); 40 | 41 | test('merging one group & one instance', withServer, async (t, server) => { 42 | server.get('/', echoHeaders); 43 | 44 | const instanceA = got.extend({headers: {dog: 'woof'}}); 45 | const instanceB = got.extend({headers: {cat: 'meow'}}); 46 | const instanceC = got.extend({headers: {bird: 'tweet'}}); 47 | const instanceD = got.extend({headers: {mouse: 'squeek'}}); 48 | 49 | const merged = instanceA.extend(instanceB, instanceC); 50 | const doubleMerged = merged.extend(instanceD); 51 | 52 | const headers = await doubleMerged(server.url).json(); 53 | t.is(headers.dog, 'woof'); 54 | t.is(headers.cat, 'meow'); 55 | t.is(headers.bird, 'tweet'); 56 | t.is(headers.mouse, 'squeek'); 57 | }); 58 | 59 | test('merging two groups of merged instances', withServer, async (t, server) => { 60 | server.get('/', echoHeaders); 61 | 62 | const instanceA = got.extend({headers: {dog: 'woof'}}); 63 | const instanceB = got.extend({headers: {cat: 'meow'}}); 64 | const instanceC = got.extend({headers: {bird: 'tweet'}}); 65 | const instanceD = got.extend({headers: {mouse: 'squeek'}}); 66 | 67 | const groupA = instanceA.extend(instanceB); 68 | const groupB = instanceC.extend(instanceD); 69 | 70 | const merged = groupA.extend(groupB); 71 | 72 | const headers = await merged(server.url).json(); 73 | t.is(headers.dog, 'woof'); 74 | t.is(headers.cat, 'meow'); 75 | t.is(headers.bird, 'tweet'); 76 | t.is(headers.mouse, 'squeek'); 77 | }); 78 | 79 | test('hooks are merged', t => { 80 | const getBeforeRequestHooks = (instance: Got): BeforeRequestHook[] => instance.defaults.options.hooks.beforeRequest; 81 | 82 | const instanceA = got.extend({ 83 | hooks: { 84 | beforeRequest: [ 85 | options => { 86 | options.headers.dog = 'woof'; 87 | }, 88 | ], 89 | }, 90 | }); 91 | const instanceB = got.extend({ 92 | hooks: { 93 | beforeRequest: [ 94 | options => { 95 | options.headers.cat = 'meow'; 96 | }, 97 | ], 98 | }, 99 | }); 100 | 101 | const merged = instanceA.extend(instanceB); 102 | t.deepEqual(getBeforeRequestHooks(merged), [...getBeforeRequestHooks(instanceA), ...getBeforeRequestHooks(instanceB)]); 103 | }); 104 | 105 | test('default handlers are not duplicated', t => { 106 | const instance = got.extend(got); 107 | t.is(instance.defaults.handlers.length, 0); 108 | }); 109 | 110 | test('URL is not polluted', withServer, async (t, server, got) => { 111 | server.get('/', (_request, response) => { 112 | response.end('ok'); 113 | }); 114 | 115 | await got({ 116 | username: 'foo', 117 | }); 118 | 119 | const {options: normalizedOptions} = (await got({})).request; 120 | 121 | t.is(normalizedOptions.username, ''); 122 | }); 123 | 124 | test('merging instances with HTTPS options', t => { 125 | const instanceA = got.extend({ 126 | https: { 127 | rejectUnauthorized: true, 128 | certificate: 'FIRST', 129 | }, 130 | }); 131 | const instanceB = got.extend({ 132 | https: { 133 | certificate: 'SECOND', 134 | }, 135 | }); 136 | 137 | const merged = instanceA.extend(instanceB); 138 | 139 | t.true(merged.defaults.options.https.rejectUnauthorized); 140 | t.is(merged.defaults.options.https.certificate, 'SECOND'); 141 | }); 142 | 143 | test('merging instances with HTTPS options undefined', t => { 144 | const instanceA = got.extend({ 145 | https: { 146 | rejectUnauthorized: true, 147 | certificate: 'FIRST', 148 | }, 149 | }); 150 | const instanceB = got.extend({ 151 | https: { 152 | certificate: undefined, 153 | }, 154 | }); 155 | 156 | const merged = instanceA.extend(instanceB); 157 | 158 | t.true(merged.defaults.options.https.rejectUnauthorized); 159 | t.is(merged.defaults.options.https.certificate, undefined); 160 | }); 161 | 162 | test('accepts options for promise API', t => { 163 | got.extend({ 164 | hooks: { 165 | beforeRequest: [ 166 | options => { 167 | options.responseType = 'buffer'; 168 | }, 169 | ], 170 | }, 171 | }); 172 | 173 | t.pass(); 174 | }); 175 | 176 | test('merging `prefixUrl`', t => { 177 | const prefixUrl = 'http://example.com/'; 178 | 179 | const instanceA = got.extend({headers: {unicorn: 'rainbow'}}); 180 | const instanceB = got.extend({prefixUrl}); 181 | const mergedAonB = instanceB.extend(instanceA); 182 | const mergedBonA = instanceA.extend(instanceB); 183 | 184 | t.is(mergedAonB.defaults.options.prefixUrl, prefixUrl); 185 | t.is(mergedBonA.defaults.options.prefixUrl, prefixUrl); 186 | 187 | t.is(instanceB.extend({}).defaults.options.prefixUrl, prefixUrl); 188 | }); 189 | -------------------------------------------------------------------------------- /test/normalize-arguments.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import got, {Options} from '../source/index.js'; 3 | 4 | test('should merge options replacing responseType', t => { 5 | const responseType = 'json'; 6 | const options = new Options({ 7 | responseType, 8 | }, undefined, got.defaults.options); 9 | 10 | t.is(options.responseType, responseType); 11 | }); 12 | 13 | test('no duplicated searchParams values', t => { 14 | const options = new Options({ 15 | searchParams: 'string=true&noDuplication=true', 16 | }, { 17 | searchParams: new URLSearchParams({ 18 | instance: 'true', 19 | noDuplication: 'true', 20 | }), 21 | }); 22 | 23 | // eslint-disable-next-line unicorn/prevent-abbreviations 24 | const searchParams = options.searchParams as URLSearchParams; 25 | 26 | t.is(searchParams.get('string'), 'true'); 27 | t.is(searchParams.get('instance'), 'true'); 28 | t.is(searchParams.getAll('noDuplication').length, 1); 29 | }); 30 | 31 | test('should copy non-numerable properties', t => { 32 | const options = { 33 | json: {hello: '123'}, 34 | }; 35 | 36 | const merged = new Options(options, undefined, got.defaults.options); 37 | const mergedTwice = new Options(undefined, undefined, merged); 38 | 39 | t.is(mergedTwice.json, options.json); 40 | }); 41 | 42 | test('should get username and password from the URL', t => { 43 | const options = new Options({ 44 | url: 'http://user:pass@localhost:41285', 45 | }); 46 | 47 | t.is(options.username, 'user'); 48 | t.is(options.password, 'pass'); 49 | }); 50 | 51 | test('should get username and password from the options', t => { 52 | const options = new Options({ 53 | url: 'http://user:pass@localhost:41285', 54 | username: 'user_OPT', 55 | password: 'pass_OPT', 56 | }); 57 | 58 | t.is(options.username, 'user_OPT'); 59 | t.is(options.password, 'pass_OPT'); 60 | }); 61 | 62 | test('should get username and password from the merged options', t => { 63 | const options = new Options( 64 | { 65 | url: 'http://user:pass@localhost:41285', 66 | }, 67 | { 68 | username: 'user_OPT_MERGE', 69 | password: 'pass_OPT_MERGE', 70 | }, 71 | ); 72 | 73 | t.is(options.username, 'user_OPT_MERGE'); 74 | t.is(options.password, 'pass_OPT_MERGE'); 75 | }); 76 | 77 | test('null value in search params means empty', t => { 78 | const options = new Options({ 79 | url: new URL('http://localhost'), 80 | searchParams: { 81 | foo: null, 82 | }, 83 | }); 84 | 85 | t.is((options.url as URL).href, 'http://localhost/?foo='); 86 | }); 87 | 88 | test('undefined value in search params means it does not exist', t => { 89 | const options = new Options({ 90 | url: new URL('http://localhost'), 91 | searchParams: { 92 | foo: undefined, 93 | }, 94 | }); 95 | 96 | t.is((options.url as URL).href, 'http://localhost/'); 97 | }); 98 | 99 | test('prefixUrl alone does not set url', t => { 100 | const options = new Options({ 101 | prefixUrl: 'https://example.com', 102 | }); 103 | 104 | t.is(options.url, undefined); 105 | }); 106 | 107 | test('maxRetryAfter is calculated separately from request timeout', t => { 108 | const options = new Options({ 109 | timeout: { 110 | request: 1000, 111 | }, 112 | retry: { 113 | maxRetryAfter: undefined, 114 | }, 115 | }); 116 | 117 | t.is(options.retry.maxRetryAfter, undefined); 118 | 119 | options.merge({ 120 | timeout: { 121 | request: 2000, 122 | }, 123 | }); 124 | 125 | t.is(options.retry.maxRetryAfter, undefined); 126 | 127 | options.merge({ 128 | retry: { 129 | maxRetryAfter: 300, 130 | }, 131 | }); 132 | 133 | t.is(options.retry.maxRetryAfter, 300); 134 | }); 135 | 136 | test('extending responseType', t => { 137 | const instance1 = got.extend({ 138 | prefixUrl: 'https://localhost', 139 | responseType: 'json', 140 | }); 141 | 142 | const instance2 = got.extend({ 143 | headers: { 144 | 'x-test': 'test', 145 | }, 146 | }); 147 | 148 | const merged = instance1.extend(instance2); 149 | 150 | t.is(merged.defaults.options.responseType, 'json'); 151 | }); 152 | 153 | test('searchParams - multiple values for one key', t => { 154 | const searchParameters = new URLSearchParams(); 155 | 156 | searchParameters.append('a', '100'); 157 | searchParameters.append('a', '200'); 158 | searchParameters.append('a', '300'); 159 | 160 | const options = new Options({ 161 | searchParams: searchParameters, 162 | }); 163 | 164 | t.deepEqual( 165 | (options.searchParams as URLSearchParams).getAll('a'), 166 | ['100', '200', '300'], 167 | ); 168 | }); 169 | 170 | if (globalThis.AbortSignal !== undefined) { 171 | test('signal does not get frozen', t => { 172 | const controller = new AbortController(); 173 | const {signal} = controller; 174 | 175 | const options = new Options({ 176 | url: new URL('http://localhost'), 177 | signal, 178 | }); 179 | options.freeze(); 180 | 181 | t.is(Object.isFrozen(options.signal), false); 182 | }); 183 | } 184 | -------------------------------------------------------------------------------- /test/parse-link-header.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import parseLinkHeader from '../source/core/parse-link-header.js'; 3 | 4 | test('works as expected', t => { 5 | t.deepEqual( 6 | parseLinkHeader( 7 | '; rel="preconnect", ; rel="preconnect", ; rel="preconnect"', 8 | ), 9 | [ 10 | { 11 | reference: 'https://one.example.com', 12 | parameters: {rel: '"preconnect"'}, 13 | }, 14 | { 15 | reference: 'https://two.example.com', 16 | parameters: {rel: '"preconnect"'}, 17 | }, 18 | { 19 | reference: 'https://three.example.com', 20 | parameters: {rel: '"preconnect"'}, 21 | }, 22 | ], 23 | ); 24 | 25 | t.deepEqual( 26 | parseLinkHeader( 27 | '; rel="previous"; title="previous chapter"', 28 | ), 29 | [ 30 | { 31 | reference: 'https://one.example.com', 32 | parameters: {rel: '"previous"', title: '"previous chapter"'}, 33 | }, 34 | ], 35 | ); 36 | 37 | t.deepEqual( 38 | parseLinkHeader('; rel="http://example.net/foo"'), 39 | [ 40 | { 41 | reference: '/', 42 | parameters: {rel: '"http://example.net/foo"'}, 43 | }, 44 | ], 45 | ); 46 | 47 | t.deepEqual( 48 | parseLinkHeader('; rel="copyright"; anchor="#foo"'), 49 | [ 50 | { 51 | reference: '/terms', 52 | parameters: {rel: '"copyright"', anchor: '"#foo"'}, 53 | }, 54 | ], 55 | ); 56 | 57 | t.deepEqual(parseLinkHeader(`; 58 | rel="previous"; title*=UTF-8'de'letztes%20Kapitel, 59 | ; 60 | rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel`), [ 61 | { 62 | reference: '/TheBook/chapter2', 63 | parameters: { 64 | rel: '"previous"', 65 | // eslint-disable-next-line @typescript-eslint/quotes, @typescript-eslint/naming-convention 66 | 'title*': `UTF-8'de'letztes%20Kapitel`, 67 | }, 68 | }, 69 | { 70 | reference: '/TheBook/chapter4', 71 | parameters: { 72 | rel: '"next"', 73 | // eslint-disable-next-line @typescript-eslint/quotes, @typescript-eslint/naming-convention 74 | 'title*': `UTF-8'de'n%c3%a4chstes%20Kapitel`, 75 | }, 76 | }, 77 | ]); 78 | 79 | t.throws(() => parseLinkHeader('https://bad.example; rel="preconnect"'), { 80 | message: 'Invalid format of the Link header reference: https://bad.example', 81 | }); 82 | 83 | t.throws(() => parseLinkHeader('https://bad.example; rel'), { 84 | message: 'Invalid format of the Link header reference: https://bad.example', 85 | }); 86 | 87 | t.throws(() => parseLinkHeader('https://bad.example'), { 88 | message: 'Invalid format of the Link header reference: https://bad.example', 89 | }); 90 | 91 | t.throws(() => parseLinkHeader(''), { 92 | message: 'Invalid format of the Link header reference: ', 93 | }); 94 | 95 | t.throws(() => parseLinkHeader('; rel'), { 96 | message: 'Failed to parse Link header: ; rel', 97 | }); 98 | 99 | t.throws(() => parseLinkHeader(''), { 100 | message: 'Unexpected end of Link header parameters: ', 101 | }); 102 | 103 | t.throws(() => parseLinkHeader('<>'), { 104 | message: 'Unexpected end of Link header parameters: ', 105 | }); 106 | 107 | t.throws(() => parseLinkHeader(' parseLinkHeader('https://bad.example>'), { 112 | message: 'Invalid format of the Link header reference: https://bad.example>', 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /test/promise.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'node:buffer'; 2 | import {ReadStream} from 'node:fs'; 3 | import {ClientRequest, IncomingMessage} from 'node:http'; 4 | import test from 'ava'; 5 | import {type Response, CancelError} from '../source/index.js'; 6 | import withServer from './helpers/with-server.js'; 7 | 8 | test('emits request event as promise', withServer, async (t, server, got) => { 9 | server.get('/', (_request, response) => { 10 | response.statusCode = 200; 11 | response.end('null'); 12 | }); 13 | 14 | await got('').json().on('request', (request: ClientRequest) => { 15 | t.true(request instanceof ClientRequest); 16 | }); 17 | }); 18 | 19 | test('emits response event as promise', withServer, async (t, server, got) => { 20 | server.get('/', (_request, response) => { 21 | response.statusCode = 200; 22 | response.end('null'); 23 | }); 24 | 25 | await got('').json().on('response', (response: Response) => { 26 | t.true(response instanceof IncomingMessage); 27 | t.false(response.readable); 28 | t.is(response.statusCode, 200); 29 | t.true(response.ip === '127.0.0.1' || response.ip === '::1'); 30 | }); 31 | }); 32 | 33 | test('returns buffer on compressed response', withServer, async (t, server, got) => { 34 | server.get('/', (_request, response) => { 35 | response.setHeader('content-encoding', 'gzip'); 36 | response.end(); 37 | }); 38 | 39 | const {body} = await got({decompress: false}); 40 | t.true(Buffer.isBuffer(body)); 41 | }); 42 | 43 | test('no unhandled `The server aborted pending request` rejection', withServer, async (t, server, got) => { 44 | server.get('/', (_request, response) => { 45 | response.statusCode = 503; 46 | response.write('asdf'); 47 | 48 | setTimeout(() => { 49 | response.end(); 50 | }, 100); 51 | }); 52 | 53 | await t.throwsAsync(got('')); 54 | }); 55 | 56 | test('promise.json() can be called before a file stream body is open', withServer, async (t, server, got) => { 57 | server.post('/', (request, response) => { 58 | request.resume(); 59 | request.once('end', () => { 60 | response.end('""'); 61 | }); 62 | }); 63 | 64 | // @ts-expect-error @types/node has wrong types. 65 | const body = new ReadStream('', { 66 | fs: { 67 | open() {}, 68 | read() {}, 69 | close() {}, 70 | }, 71 | }); 72 | 73 | const promise = got({body}); 74 | const checks = [ 75 | t.throwsAsync(promise, { 76 | instanceOf: CancelError, 77 | code: 'ERR_CANCELED', 78 | }), 79 | t.throwsAsync(promise.json(), { 80 | instanceOf: CancelError, 81 | code: 'ERR_CANCELED', 82 | }), 83 | ]; 84 | 85 | promise.cancel(); 86 | 87 | await Promise.all(checks); 88 | }); 89 | 90 | test('promise.json() does not fail when server returns an error', withServer, async (t, server, got) => { 91 | server.get('/', (_request, response) => { 92 | response.statusCode = 400; 93 | response.end('{}'); 94 | }); 95 | 96 | const promise = got('', {throwHttpErrors: false}); 97 | await t.notThrowsAsync(promise.json()); 98 | }); 99 | -------------------------------------------------------------------------------- /test/timings.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import got from '../source/index.js'; 3 | import withServer from './helpers/with-server.js'; 4 | 5 | test('http/1 timings', withServer, async (t, server, got) => { 6 | server.get('/', (_request, response) => { 7 | response.end('ok'); 8 | }); 9 | 10 | const {timings} = await got(''); 11 | 12 | t.true(timings.start >= 0); 13 | t.true(timings.socket! >= 0); 14 | t.true(timings.lookup! >= 0); 15 | t.true(timings.connect! >= 0); 16 | t.true(timings.upload! >= 0); 17 | t.true(timings.response! >= 0); 18 | t.true(timings.end! >= 0); 19 | 20 | const {phases} = timings; 21 | 22 | t.true(phases.wait! >= 0); 23 | t.true(phases.dns! >= 0); 24 | t.true(phases.tcp! >= 0); 25 | t.true(phases.request! >= 0); 26 | t.true(phases.firstByte! >= 0); 27 | t.true(phases.download! >= 0); 28 | t.true(phases.total! >= 0); 29 | }); 30 | 31 | test.failing('http/2 timings', async t => { 32 | const {timings} = await got('https://httpbin.org/anything', {http2: true}); 33 | 34 | t.true(timings.start >= 0); 35 | t.true(timings.socket! >= 0); 36 | t.true(timings.lookup! >= 0); 37 | t.true(timings.connect! >= 0); 38 | t.true(timings.secureConnect! >= 0); 39 | t.true(timings.upload! >= 0); 40 | t.true(timings.response! >= 0); 41 | t.true(timings.end! >= 0); 42 | 43 | const {phases} = timings; 44 | 45 | t.true(phases.wait! >= 0); 46 | t.true(phases.dns! >= 0); 47 | t.true(phases.tcp! >= 0); 48 | t.true(phases.tls! >= 0); 49 | t.true(phases.request! >= 0); 50 | t.true(phases.firstByte! >= 0); 51 | t.true(phases.download! >= 0); 52 | t.true(phases.total! >= 0); 53 | }); 54 | -------------------------------------------------------------------------------- /test/types/create-test-server/index.d.ts: -------------------------------------------------------------------------------- 1 | import type {Buffer} from 'node:buffer'; 2 | 3 | declare module 'create-test-server' { 4 | import type {Express} from 'express'; 5 | 6 | function createTestServer(options: unknown): Promise; 7 | 8 | export = createTestServer; 9 | 10 | namespace createTestServer { 11 | export type TestServer = { 12 | caCert: string | Buffer | Array; 13 | port: number; 14 | url: string; 15 | sslPort: number; 16 | sslUrl: string; 17 | 18 | close: () => Promise; 19 | } & Express; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/types/pem.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CertificateCreationOptions, 3 | CertificateCreationResult, 4 | PrivateKeyCreationOptions, 5 | CSRCreationOptions, 6 | Callback, 7 | } from 'pem'; 8 | 9 | export type CreateCertificate = (options: CertificateCreationOptions, callback: Callback) => void; 10 | 11 | export type CreateCsr = (options: CSRCreationOptions, callback: Callback<{csr: string; clientKey: string}>) => void; 12 | 13 | export type CreatePrivateKey = (keyBitsize: number, options: PrivateKeyCreationOptions, callback: Callback<{key: string}>) => void; 14 | -------------------------------------------------------------------------------- /test/types/slow-stream/index.d.ts: -------------------------------------------------------------------------------- 1 | import {PassThrough} from 'node:stream'; 2 | 3 | declare module 'slow-stream' { 4 | export = PassThrough; 5 | } 6 | -------------------------------------------------------------------------------- /test/unix-socket.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {format} from 'node:util'; 3 | import test from 'ava'; 4 | import type {Handler} from 'express'; 5 | import baseGot from '../source/index.js'; 6 | import {withSocketServer} from './helpers/with-server.js'; 7 | 8 | const got = baseGot.extend({enableUnixSockets: true}); 9 | 10 | const okHandler: Handler = (_request, response) => { 11 | response.end('ok'); 12 | }; 13 | 14 | const redirectHandler: Handler = (_request, response) => { 15 | response.writeHead(302, { 16 | location: 'foo', 17 | }); 18 | response.end(); 19 | }; 20 | 21 | if (process.platform !== 'win32') { 22 | test('works', withSocketServer, async (t, server) => { 23 | server.on('/', okHandler); 24 | 25 | const url = format('http://unix:%s:%s', server.socketPath, '/'); 26 | t.is((await got(url, {})).body, 'ok'); 27 | }); 28 | 29 | test('protocol-less works', withSocketServer, async (t, server) => { 30 | server.on('/', okHandler); 31 | 32 | const url = format('unix:%s:%s', server.socketPath, '/'); 33 | t.is((await got(url)).body, 'ok'); 34 | }); 35 | 36 | test('address with : works', withSocketServer, async (t, server) => { 37 | server.on('/foo:bar', okHandler); 38 | 39 | const url = format('unix:%s:%s', server.socketPath, '/foo:bar'); 40 | t.is((await got(url)).body, 'ok'); 41 | }); 42 | 43 | test('throws on invalid URL', async t => { 44 | try { 45 | await got('unix:', {retry: {limit: 0}}); 46 | } catch (error: any) { 47 | t.regex(error.code, /ENOTFOUND|EAI_AGAIN/); 48 | } 49 | }); 50 | 51 | test('works when extending instances', withSocketServer, async (t, server) => { 52 | server.on('/', okHandler); 53 | 54 | const url = format('unix:%s:%s', server.socketPath, '/'); 55 | const instance = got.extend({prefixUrl: url}); 56 | t.is((await instance('')).body, 'ok'); 57 | }); 58 | 59 | test('passes search params', withSocketServer, async (t, server) => { 60 | server.on('/?a=1', okHandler); 61 | 62 | const url = format('http://unix:%s:%s', server.socketPath, '/?a=1'); 63 | t.is((await got(url)).body, 'ok'); 64 | }); 65 | 66 | test('redirects work', withSocketServer, async (t, server) => { 67 | server.on('/', redirectHandler); 68 | server.on('/foo', okHandler); 69 | 70 | const url = format('http://unix:%s:%s', server.socketPath, '/'); 71 | t.is((await got(url)).body, 'ok'); 72 | }); 73 | 74 | test('`unix:` fails when UNIX sockets are not enabled', async t => { 75 | const gotUnixSocketsDisabled = got.extend({enableUnixSockets: false}); 76 | 77 | t.false(gotUnixSocketsDisabled.defaults.options.enableUnixSockets); 78 | await t.throwsAsync( 79 | gotUnixSocketsDisabled('unix:'), 80 | { 81 | message: 'Using UNIX domain sockets but option `enableUnixSockets` is not enabled', 82 | }, 83 | ); 84 | }); 85 | 86 | test('`http://unix:/` fails when UNIX sockets are not enabled', async t => { 87 | const gotUnixSocketsDisabled = got.extend({enableUnixSockets: false}); 88 | 89 | t.false(gotUnixSocketsDisabled.defaults.options.enableUnixSockets); 90 | 91 | await t.throwsAsync( 92 | gotUnixSocketsDisabled('http://unix:'), 93 | { 94 | message: 'Using UNIX domain sockets but option `enableUnixSockets` is not enabled', 95 | }, 96 | ); 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /test/url-to-options.ts: -------------------------------------------------------------------------------- 1 | import {parse as urlParse} from 'node:url'; 2 | import test from 'ava'; 3 | import urlToOptions from '../source/core/utils/url-to-options.js'; 4 | 5 | test('converts node legacy URL to options', t => { 6 | const exampleUrl = 'https://user:password@github.com:443/say?hello=world#bang'; 7 | const parsedUrl = urlParse(exampleUrl); 8 | const options = urlToOptions(parsedUrl); 9 | const expected = { 10 | hash: '#bang', 11 | host: 'github.com:443', 12 | hostname: 'github.com', 13 | href: exampleUrl, 14 | path: '/say?hello=world', 15 | pathname: '/say', 16 | port: 443, 17 | protocol: 'https:', 18 | search: '?hello=world', 19 | }; 20 | 21 | t.deepEqual(options, expected); 22 | }); 23 | 24 | test('converts URL to options', t => { 25 | const exampleUrl = 'https://user:password@github.com:443/say?hello=world#bang'; 26 | const parsedUrl = new URL(exampleUrl); 27 | const options = urlToOptions(parsedUrl); 28 | const expected = { 29 | auth: 'user:password', 30 | hash: '#bang', 31 | host: 'github.com', 32 | hostname: 'github.com', 33 | href: 'https://user:password@github.com/say?hello=world#bang', 34 | path: '/say?hello=world', 35 | pathname: '/say', 36 | protocol: 'https:', 37 | search: '?hello=world', 38 | }; 39 | 40 | t.deepEqual(options, expected); 41 | }); 42 | 43 | test('converts IPv6 URL to options', t => { 44 | // eslint-disable-next-line @typescript-eslint/naming-convention 45 | const IPv6Url = 'https://[2001:cdba::3257:9652]:443/'; 46 | const parsedUrl = new URL(IPv6Url); 47 | const options = urlToOptions(parsedUrl); 48 | const expected = { 49 | hash: '', 50 | host: '[2001:cdba::3257:9652]', 51 | hostname: '2001:cdba::3257:9652', 52 | href: 'https://[2001:cdba::3257:9652]/', 53 | path: '/', 54 | pathname: '/', 55 | protocol: 'https:', 56 | search: '', 57 | }; 58 | 59 | t.deepEqual(options, expected); 60 | }); 61 | 62 | test('only adds port to options for URLs with ports', t => { 63 | const noPortUrl = 'https://github.com/'; 64 | const parsedUrl = new URL(noPortUrl); 65 | const options = urlToOptions(parsedUrl); 66 | const expected = { 67 | hash: '', 68 | host: 'github.com', 69 | hostname: 'github.com', 70 | href: 'https://github.com/', 71 | path: '/', 72 | pathname: '/', 73 | protocol: 'https:', 74 | search: '', 75 | }; 76 | 77 | t.deepEqual(options, expected); 78 | t.false(Reflect.has(options, 'port')); 79 | }); 80 | 81 | test('does not concat null search to path', t => { 82 | const exampleUrl = 'https://github.com/'; 83 | const parsedUrl = urlParse(exampleUrl); 84 | 85 | t.is(parsedUrl.search, null); 86 | 87 | const options = urlToOptions(parsedUrl); 88 | const expected = { 89 | hash: null, 90 | host: 'github.com', 91 | hostname: 'github.com', 92 | href: 'https://github.com/', 93 | path: '/', 94 | pathname: '/', 95 | protocol: 'https:', 96 | search: null, 97 | }; 98 | 99 | t.deepEqual(options, expected); 100 | }); 101 | 102 | test('does not add null port to options', t => { 103 | const exampleUrl = 'https://github.com/'; 104 | const parsedUrl = urlParse(exampleUrl); 105 | 106 | t.is(parsedUrl.port, null); 107 | 108 | const options = urlToOptions(parsedUrl); 109 | const expected = { 110 | hash: null, 111 | host: 'github.com', 112 | hostname: 'github.com', 113 | href: 'https://github.com/', 114 | path: '/', 115 | pathname: '/', 116 | protocol: 'https:', 117 | search: null, 118 | }; 119 | 120 | t.deepEqual(options, expected); 121 | }); 122 | 123 | test('does not throw if there is no hostname', t => { 124 | t.notThrows(() => urlToOptions({} as URL)); 125 | }); 126 | 127 | test('null password', t => { 128 | const options = urlToOptions({ 129 | username: 'foo', 130 | password: null, 131 | } as any); 132 | 133 | t.is(options.auth, 'foo:'); 134 | }); 135 | 136 | test('null username', t => { 137 | const options = urlToOptions({ 138 | username: null, 139 | password: 'bar', 140 | } as any); 141 | 142 | t.is(options.auth, ':bar'); 143 | }); 144 | -------------------------------------------------------------------------------- /test/weakable-map.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import WeakableMap from '../source/core/utils/weakable-map.js'; 3 | 4 | test('works as expected', t => { 5 | const weakable = new WeakableMap(); 6 | 7 | weakable.set('hello', 'world'); 8 | 9 | t.true(weakable.has('hello')); 10 | t.false(weakable.has('foobar')); 11 | t.is(weakable.get('hello'), 'world'); 12 | t.is(weakable.get('foobar'), undefined); 13 | 14 | const object = {}; 15 | const anotherObject = {}; 16 | weakable.set(object, 'world'); 17 | 18 | t.true(weakable.has(object)); 19 | t.false(weakable.has(anotherObject)); 20 | t.is(weakable.get(object), 'world'); 21 | t.is(weakable.get(anotherObject), undefined); 22 | }); 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sindresorhus/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2022", // Node.js 18 6 | "lib": [ 7 | "es2022" 8 | ], 9 | "noPropertyAccessFromIndexSignature": false, 10 | "isolatedModules": true 11 | }, 12 | "include": [ 13 | "source", 14 | "test", 15 | "benchmark" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------