├── .npmrc ├── test ├── fixtures │ └── stream-content-length ├── helpers │ ├── with-server.ts │ └── server.ts ├── timings.ts ├── helpers.ts ├── https.ts ├── socket-destroyed.ts ├── promise.ts ├── unix-socket.ts ├── query.ts ├── gzip.ts ├── url-to-options.ts ├── cookies.ts ├── http.ts ├── response-parse.ts ├── agent.ts ├── post.ts ├── cancel.ts ├── cache.ts ├── merge-instances.ts ├── progress.ts ├── error.ts ├── stream.ts ├── create.ts ├── headers.ts ├── arguments.ts ├── redirects.ts ├── retry.ts ├── hooks.ts └── timeout.ts ├── .gitattributes ├── .gitignore ├── media ├── logo.ai ├── logo.png ├── logo.sketch └── logo.svg ├── source ├── utils │ ├── supports-brotli.ts │ ├── is-form-data.ts │ ├── deep-freeze.ts │ ├── validate-search-params.ts │ ├── get-body-size.ts │ ├── url-to-options.ts │ ├── types.ts │ └── timed-out.ts ├── index.ts ├── get-response.ts ├── create.ts ├── as-stream.ts ├── progress.ts ├── merge.ts ├── errors.ts ├── as-promise.ts ├── known-hook-events.ts ├── normalize-arguments.ts └── request-as-event-emitter.ts ├── .travis.yml ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── 3-question.md │ ├── 2-feature-request.md │ └── 1-bug-report.md └── PULL_REQUEST_TEMPLATE.md ├── tsconfig.json ├── license ├── package.json ├── migration-guides.md └── advanced-creation.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /test/fixtures/stream-content-length: -------------------------------------------------------------------------------- 1 | Unicorns 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.ai binary 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | coverage 4 | .nyc_output 5 | dist 6 | -------------------------------------------------------------------------------- /media/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/got/master/media/logo.ai -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/got/master/media/logo.png -------------------------------------------------------------------------------- /media/logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/got/master/media/logo.sketch -------------------------------------------------------------------------------- /source/utils/supports-brotli.ts: -------------------------------------------------------------------------------- 1 | import zlib from 'zlib'; 2 | 3 | export default typeof (zlib as any).createBrotliDecompress === 'function'; 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '8' 5 | after_success: 6 | - './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls' 7 | -------------------------------------------------------------------------------- /source/utils/is-form-data.ts: -------------------------------------------------------------------------------- 1 | import is from '@sindresorhus/is'; 2 | import FormData from 'form-data'; 3 | 4 | export default (body: unknown): body is FormData => is.nodeStream(body) && is.function_((body as FormData).getBoundary); 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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. 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sindresorhus/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2017", // Node.js 8 6 | 7 | // TODO: Make it strict 8 | "strict": false, 9 | "noImplicitReturns": false, 10 | "noUnusedParameters": false 11 | }, 12 | "include": [ 13 | "source" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /source/utils/deep-freeze.ts: -------------------------------------------------------------------------------- 1 | import is from '@sindresorhus/is'; 2 | 3 | export default function deepFreeze(object: T): Readonly { 4 | for (const value of Object.values(object)) { 5 | if (is.plainObject(value) || is.array(value)) { 6 | deepFreeze(value); 7 | } 8 | } 9 | 10 | return Object.freeze(object); 11 | } 12 | -------------------------------------------------------------------------------- /test/helpers/with-server.ts: -------------------------------------------------------------------------------- 1 | import {URL} from 'url'; 2 | import createTestServer from 'create-test-server'; 3 | 4 | export default async (t, run) => { 5 | const server = await createTestServer(); 6 | 7 | server.hostname = (new URL(server.url)).hostname; 8 | 9 | try { 10 | await run(t, server); 11 | } finally { 12 | await server.close(); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /test/timings.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import got from '../source'; 3 | import withServer from './helpers/with-server'; 4 | 5 | // #687 6 | test('sensible timings', withServer, async (t, s) => { 7 | s.get('/', (request, response) => { 8 | response.end('ok'); 9 | }); 10 | const {timings} = await got(s.url); 11 | t.true(timings.phases.request < 1000); 12 | }); 13 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /source/utils/validate-search-params.ts: -------------------------------------------------------------------------------- 1 | import is from '@sindresorhus/is'; 2 | 3 | const verify = (value: unknown, type: string) => { 4 | if (!is.string(value) && !is.number(value) && !is.boolean(value) && !is.null_(value)) { 5 | throw new TypeError(`The \`searchParams\` ${type} '${value}' must be a string, number, boolean or null`); 6 | } 7 | }; 8 | 9 | export default (searchParams: Record) => { 10 | for (const [key, value] of Object.entries(searchParams)) { 11 | verify(key, 'key'); 12 | verify(value, 'value'); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import got from '../source'; 3 | import withServer from './helpers/with-server'; 4 | 5 | test('promise mode', withServer, async (t, s) => { 6 | s.get('/', (request, response) => { 7 | response.end('ok'); 8 | }); 9 | s.get('/404', (request, response) => { 10 | response.statusCode = 404; 11 | response.end('not found'); 12 | }); 13 | 14 | t.is((await got.get(s.url)).body, 'ok'); 15 | 16 | const error = await t.throwsAsync(got.get(`${s.url}/404`)); 17 | t.is(error.response.body, 'not found'); 18 | 19 | const error2 = await t.throwsAsync(got.get('.com', {retry: 0})); 20 | t.truthy(error2); 21 | }); 22 | -------------------------------------------------------------------------------- /test/https.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import got from '../source'; 3 | import {createSSLServer} from './helpers/server'; 4 | 5 | let s; 6 | 7 | test.before('setup', async () => { 8 | s = await createSSLServer(); 9 | 10 | s.on('/', (request_, response) => response.end('ok')); 11 | 12 | await s.listen(s.port); 13 | }); 14 | 15 | test.after('cleanup', async () => { 16 | await s.close(); 17 | }); 18 | 19 | test('make request to https server without ca', async t => { 20 | t.truthy((await got(s.url, {rejectUnauthorized: false})).body); 21 | }); 22 | 23 | test('make request to https server with ca', async t => { 24 | const {body} = await got(s.url, { 25 | ca: s.caRootCert, 26 | headers: {host: 'sindresorhus.com'} 27 | }); 28 | t.is(body, 'ok'); 29 | }); 30 | -------------------------------------------------------------------------------- /source/utils/get-body-size.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import {promisify} from 'util'; 3 | import is from '@sindresorhus/is'; 4 | import isFormData from './is-form-data'; 5 | 6 | export default async (options: any): Promise => { 7 | const {body} = options; 8 | 9 | if (options.headers['content-length']) { 10 | return Number(options.headers['content-length']); 11 | } 12 | 13 | if (!body && !options.stream) { 14 | return 0; 15 | } 16 | 17 | if (is.string(body)) { 18 | return Buffer.byteLength(body); 19 | } 20 | 21 | if (isFormData(body)) { 22 | return promisify(body.getLength.bind(body))(); 23 | } 24 | 25 | if (body instanceof fs.ReadStream) { 26 | const {size} = await promisify(fs.stat)(body.path); 27 | return size; 28 | } 29 | 30 | return undefined; 31 | }; 32 | -------------------------------------------------------------------------------- /test/socket-destroyed.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import got from '../source'; 3 | 4 | // TODO: Use `getActiveResources()` instead when it's out: 5 | // https://github.com/nodejs/node/pull/21453 6 | // @ts-ignore 7 | const {Timer} = process.binding('timer_wrap'); // eslint-disable-line node/no-deprecated-api 8 | 9 | test.serial('clear the progressInterval if the socket has been destroyed', async t => { 10 | const error = await t.throwsAsync(got('http://127.0.0.1:55555', {retry: 0})); 11 | // @ts-ignore 12 | const progressIntervalTimer = process._getActiveHandles().filter(handle => { 13 | // Check if the handle is a Timer that matches the `uploadEventFrequency` interval 14 | return handle instanceof Timer && handle._list.msecs === 150; 15 | }); 16 | t.is(progressIntervalTimer.length, 0); 17 | // @ts-ignore 18 | t.is(error.code, 'ECONNREFUSED'); 19 | }); 20 | -------------------------------------------------------------------------------- /test/promise.ts: -------------------------------------------------------------------------------- 1 | import {ClientRequest} from 'http'; 2 | import {Transform as TransformStream} from 'stream'; 3 | import test from 'ava'; 4 | import got from '../source'; 5 | import withServer from './helpers/with-server'; 6 | 7 | test('should emit request event as promise', withServer, async (t, s) => { 8 | s.get('/', (request, response) => { 9 | response.statusCode = 200; 10 | response.end(); 11 | }); 12 | await got(s.url).json().on('request', request => { 13 | t.true(request instanceof ClientRequest); 14 | }); 15 | }); 16 | 17 | test('should emit response event as promise', withServer, async (t, s) => { 18 | s.get('/', (request, response) => { 19 | response.statusCode = 200; 20 | response.end(); 21 | }); 22 | await got(s.url).json().on('response', response => { 23 | t.true(response instanceof TransformStream); 24 | t.true(response.readable); 25 | t.is(response.statusCode, 200); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /media/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/utils/url-to-options.ts: -------------------------------------------------------------------------------- 1 | import is from '@sindresorhus/is'; 2 | 3 | // TODO: Deprecate legacy Url at some point 4 | 5 | export interface URLOptions { 6 | protocol: string; 7 | hostname: string; 8 | host: string; 9 | hash: string; 10 | search: string; 11 | pathname: string; 12 | href: string; 13 | path: string; 14 | port?: number; 15 | auth?: string; 16 | } 17 | 18 | export default (url: any): URLOptions => { 19 | const options: URLOptions = { 20 | protocol: url.protocol, 21 | hostname: url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname, 22 | host: url.host, 23 | hash: url.hash, 24 | search: url.search, 25 | pathname: url.pathname, 26 | href: url.href, 27 | path: is.null_(url.search) ? url.pathname : `${url.pathname}${url.search}` 28 | }; 29 | 30 | if (is.string(url.port) && url.port.length > 0) { 31 | options.port = Number(url.port); 32 | } 33 | 34 | if (url.username || url.password) { 35 | options.auth = `${url.username}:${url.password}`; 36 | } 37 | 38 | return options; 39 | }; 40 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (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 | -------------------------------------------------------------------------------- /test/unix-socket.ts: -------------------------------------------------------------------------------- 1 | import {format} from 'util'; 2 | import tempy from 'tempy'; 3 | import test from 'ava'; 4 | import got from '../source'; 5 | import {createServer} from './helpers/server'; 6 | 7 | const socketPath = tempy.file({extension: 'socket'}); 8 | 9 | let s; 10 | 11 | if (process.platform !== 'win32') { 12 | test.before('setup', async () => { 13 | s = await createServer(); 14 | 15 | s.on('/', (request, response) => { 16 | response.end('ok'); 17 | }); 18 | 19 | s.on('/foo:bar', (request, response) => { 20 | response.end('ok'); 21 | }); 22 | 23 | await s.listen(socketPath); 24 | }); 25 | 26 | test.after('cleanup', async () => { 27 | await s.close(); 28 | }); 29 | 30 | test('works', async t => { 31 | const url = format('http://unix:%s:%s', socketPath, '/'); 32 | t.is((await got(url)).body, 'ok'); 33 | }); 34 | 35 | test('protocol-less works', async t => { 36 | const url = format('unix:%s:%s', socketPath, '/'); 37 | t.is((await got(url)).body, 'ok'); 38 | }); 39 | 40 | test('address with : works', async t => { 41 | const url = format('unix:%s:%s', socketPath, '/foo:bar'); 42 | t.is((await got(url)).body, 'ok'); 43 | }); 44 | 45 | test('throws on invalid URL', async t => { 46 | await t.throwsAsync(got('unix:')); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | import packageJson from '../package.json'; 2 | import create from './create'; 3 | 4 | const defaults = { 5 | options: { 6 | method: 'GET', 7 | retry: { 8 | retries: 2, 9 | methods: [ 10 | 'GET', 11 | 'PUT', 12 | 'HEAD', 13 | 'DELETE', 14 | 'OPTIONS', 15 | 'TRACE' 16 | ], 17 | statusCodes: [ 18 | 408, 19 | 413, 20 | 429, 21 | 500, 22 | 502, 23 | 503, 24 | 504 25 | ], 26 | errorCodes: [ 27 | 'ETIMEDOUT', 28 | 'ECONNRESET', 29 | 'EADDRINUSE', 30 | 'ECONNREFUSED', 31 | 'EPIPE', 32 | 'ENOTFOUND', 33 | 'ENETUNREACH', 34 | 'EAI_AGAIN' 35 | ] 36 | }, 37 | headers: { 38 | 'user-agent': `${packageJson.name}/${packageJson.version} (https://github.com/sindresorhus/got)` 39 | }, 40 | hooks: { 41 | beforeRequest: [], 42 | beforeRedirect: [], 43 | beforeRetry: [], 44 | afterResponse: [] 45 | }, 46 | decompress: true, 47 | throwHttpErrors: true, 48 | followRedirect: true, 49 | stream: false, 50 | cache: false, 51 | dnsCache: false, 52 | useElectronNet: false, 53 | responseType: 'text', 54 | resolveBodyOnly: false 55 | }, 56 | mutableDefaults: false 57 | }; 58 | 59 | const got = create(defaults); 60 | 61 | module.exports = got; // For CommonJS default export support 62 | export default got; 63 | -------------------------------------------------------------------------------- /source/get-response.ts: -------------------------------------------------------------------------------- 1 | import {IncomingMessage} from 'http'; 2 | import EventEmitter from 'events'; 3 | import {Transform as TransformStream} from 'stream'; 4 | import is from '@sindresorhus/is'; 5 | import decompressResponse from 'decompress-response'; 6 | import mimicResponse from 'mimic-response'; 7 | import {Options, Response} from './utils/types'; 8 | import {downloadProgress} from './progress'; 9 | 10 | export default (response: IncomingMessage, options: Options, emitter: EventEmitter) => { 11 | const downloadBodySize = Number(response.headers['content-length']) || undefined; 12 | 13 | const progressStream: TransformStream = downloadProgress(response, emitter, downloadBodySize); 14 | 15 | mimicResponse(response, progressStream); 16 | 17 | const newResponse = ( 18 | options.decompress === true && 19 | is.function_(decompressResponse) && 20 | options.method !== 'HEAD' ? decompressResponse(progressStream as unknown as IncomingMessage) : progressStream 21 | ) as Response; 22 | 23 | if (!options.decompress && ['gzip', 'deflate', 'br'].includes(response.headers['content-encoding'] || '')) { 24 | options.encoding = null; 25 | } 26 | 27 | emitter.emit('response', newResponse); 28 | 29 | emitter.emit('downloadProgress', { 30 | percent: 0, 31 | transferred: 0, 32 | total: downloadBodySize 33 | }); 34 | 35 | response.pipe(progressStream); 36 | }; 37 | -------------------------------------------------------------------------------- /test/helpers/server.ts: -------------------------------------------------------------------------------- 1 | import util from 'util'; 2 | import http from 'http'; 3 | import https from 'https'; 4 | import getPort from 'get-port'; 5 | import pem from 'pem'; 6 | 7 | export const host = 'localhost'; 8 | 9 | const createCertificate = util.promisify(pem.createCertificate); 10 | 11 | export const createServer = async () => { 12 | const port = await getPort(); 13 | 14 | const server = http.createServer((request, response) => { 15 | const event = decodeURI(request.url); 16 | if (server.listeners(event).length === 0) { 17 | response.writeHead(404, 'Not Found'); 18 | response.end(`No listener for ${event}`); 19 | } else { 20 | server.emit(event, request, response); 21 | } 22 | }) as any; 23 | 24 | server.host = host; 25 | server.port = port; 26 | server.url = `http://${host}:${port}`; 27 | server.protocol = 'http'; 28 | 29 | server.listen = util.promisify(server.listen); 30 | server.close = util.promisify(server.close); 31 | 32 | return server; 33 | }; 34 | 35 | export const createSSLServer = async () => { 36 | const port = await getPort(); 37 | 38 | const caKeys = await createCertificate({ 39 | days: 1, 40 | selfSigned: true 41 | }); 42 | 43 | const caRootKey = caKeys.serviceKey; 44 | const caRootCert = caKeys.certificate; 45 | 46 | const keys = await createCertificate({ 47 | serviceCertificate: caRootCert, 48 | serviceKey: caRootKey, 49 | serial: Date.now(), 50 | days: 500, 51 | country: '', 52 | state: '', 53 | locality: '', 54 | organization: '', 55 | organizationUnit: '', 56 | commonName: 'sindresorhus.com' 57 | }); 58 | 59 | const key = keys.clientKey; 60 | const cert = keys.certificate; 61 | 62 | const server = https.createServer({cert, key}, (request, response) => { 63 | server.emit(request.url, request, response); 64 | }) as any; 65 | 66 | server.host = host; 67 | server.port = port; 68 | server.url = `https://${host}:${port}`; 69 | server.protocol = 'https'; 70 | server.caRootCert = caRootCert; 71 | 72 | server.listen = util.promisify(server.listen); 73 | server.close = util.promisify(server.close); 74 | 75 | return server; 76 | }; 77 | -------------------------------------------------------------------------------- /source/create.ts: -------------------------------------------------------------------------------- 1 | import * as errors from './errors'; 2 | import asStream from './as-stream'; 3 | import asPromise from './as-promise'; 4 | import {normalizeArguments, preNormalizeArguments} from './normalize-arguments'; 5 | import merge, {mergeOptions, mergeInstances} from './merge'; 6 | import deepFreeze from './utils/deep-freeze'; 7 | 8 | const getPromiseOrStream = options => options.stream ? asStream(options) : asPromise(options); 9 | 10 | const aliases = [ 11 | 'get', 12 | 'post', 13 | 'put', 14 | 'patch', 15 | 'head', 16 | 'delete' 17 | ]; 18 | 19 | const create = defaults => { 20 | defaults = merge({}, defaults); 21 | preNormalizeArguments(defaults.options); 22 | 23 | if (!defaults.handler) { 24 | // This can't be getPromiseOrStream, because when merging 25 | // the chain would stop at this point and no further handlers would be called. 26 | defaults.handler = (options, next) => next(options); 27 | } 28 | 29 | function got(url, options?: any) { 30 | try { 31 | return defaults.handler(normalizeArguments(url, options, defaults), getPromiseOrStream); 32 | } catch (error) { 33 | if (options && options.stream) { 34 | throw error; 35 | } else { 36 | return Promise.reject(error); 37 | } 38 | } 39 | } 40 | 41 | got.create = create; 42 | got.extend = options => { 43 | let mutableDefaults; 44 | if (options && Reflect.has(options, 'mutableDefaults')) { 45 | mutableDefaults = options.mutableDefaults; 46 | delete options.mutableDefaults; 47 | } else { 48 | mutableDefaults = defaults.mutableDefaults; 49 | } 50 | 51 | return create({ 52 | options: mergeOptions(defaults.options, options), 53 | handler: defaults.handler, 54 | mutableDefaults 55 | }); 56 | }; 57 | 58 | got.mergeInstances = (...args) => create(mergeInstances(args)); 59 | 60 | got.stream = (url, options?: any) => got(url, {...options, stream: true}); 61 | 62 | for (const method of aliases) { 63 | got[method] = (url, options?: any) => got(url, {...options, method}); 64 | got.stream[method] = (url, options?: any) => got.stream(url, {...options, method}); 65 | } 66 | 67 | Object.assign(got, {...errors, mergeOptions}); 68 | Object.defineProperty(got, 'defaults', { 69 | value: defaults.mutableDefaults ? defaults : deepFreeze(defaults), 70 | writable: defaults.mutableDefaults, 71 | configurable: defaults.mutableDefaults, 72 | enumerable: true 73 | }); 74 | 75 | return got as any; 76 | }; 77 | 78 | export default create; 79 | -------------------------------------------------------------------------------- /test/query.ts: -------------------------------------------------------------------------------- 1 | import {URLSearchParams} from 'url'; 2 | import test from 'ava'; 3 | import got from '../source'; 4 | import {createServer} from './helpers/server'; 5 | 6 | // TODO: Remove this file before the Got v11 release together with completely removing the `query` option 7 | 8 | let s; 9 | 10 | test.before('setup', async () => { 11 | s = await createServer(); 12 | 13 | const echoUrl = (request, response) => { 14 | response.end(request.url); 15 | }; 16 | 17 | s.on('/', (request, response) => { 18 | response.statusCode = 404; 19 | response.end(); 20 | }); 21 | 22 | s.on('/test', echoUrl); 23 | s.on('/?test=wow', echoUrl); 24 | s.on('/?test=it’s+ok', echoUrl); 25 | 26 | s.on('/reached', (request, response) => { 27 | response.end('reached'); 28 | }); 29 | 30 | s.on('/relativeQuery?bang', (request, response) => { 31 | response.writeHead(302, { 32 | location: '/reached' 33 | }); 34 | response.end(); 35 | }); 36 | 37 | s.on('/?recent=true', (request, response) => { 38 | response.end('recent'); 39 | }); 40 | 41 | await s.listen(s.port); 42 | }); 43 | 44 | test.after('cleanup', async () => { 45 | await s.close(); 46 | }); 47 | 48 | test('overrides query from options', async t => { 49 | const {body} = await got( 50 | `${s.url}/?drop=this`, 51 | { 52 | query: { 53 | test: 'wow' 54 | }, 55 | cache: { 56 | get(key) { 57 | t.is(key, `cacheable-request:GET:${s.url}/?test=wow`); 58 | }, 59 | set(key) { 60 | t.is(key, `cacheable-request:GET:${s.url}/?test=wow`); 61 | } 62 | } 63 | } 64 | ); 65 | 66 | t.is(body, '/?test=wow'); 67 | }); 68 | 69 | test('escapes query parameter values', async t => { 70 | const {body} = await got(`${s.url}`, { 71 | query: { 72 | test: 'it’s ok' 73 | } 74 | }); 75 | 76 | t.is(body, '/?test=it%E2%80%99s+ok'); 77 | }); 78 | 79 | test('the `query` option can be a URLSearchParams', async t => { 80 | const query = new URLSearchParams({test: 'wow'}); 81 | const {body} = await got(s.url, {query}); 82 | t.is(body, '/?test=wow'); 83 | }); 84 | 85 | test('should ignore empty query object', async t => { 86 | t.is((await got(`${s.url}/test`, {query: {}})).requestUrl, `${s.url}/test`); 87 | }); 88 | 89 | test('query option', async t => { 90 | t.is((await got(s.url, {query: {recent: true}})).body, 'recent'); 91 | t.is((await got(s.url, {query: 'recent=true'})).body, 'recent'); 92 | }); 93 | 94 | test('query in options are not breaking redirects', async t => { 95 | t.is((await got(`${s.url}/relativeQuery`, {query: 'bang'})).body, 'reached'); 96 | }); 97 | -------------------------------------------------------------------------------- /source/as-stream.ts: -------------------------------------------------------------------------------- 1 | import {ClientRequest} from 'http'; 2 | import {PassThrough as PassThroughStream} from 'stream'; 3 | import duplexer3 from 'duplexer3'; 4 | import requestAsEventEmitter from './request-as-event-emitter'; 5 | import {HTTPError, ReadError} from './errors'; 6 | import {MergedOptions, Response} from './utils/types'; 7 | 8 | export default function asStream(options: MergedOptions) { 9 | const input = new PassThroughStream(); 10 | const output = new PassThroughStream(); 11 | const proxy = duplexer3(input, output); 12 | const piped = new Set(); 13 | let isFinished = false; 14 | 15 | options.retry.retries = () => 0; 16 | 17 | if (options.body) { 18 | proxy.write = () => { 19 | throw new Error('Got\'s stream is not writable when the `body` option is used'); 20 | }; 21 | } 22 | 23 | const emitter = requestAsEventEmitter(options, input) as ClientRequest; 24 | 25 | // Cancels the request 26 | proxy._destroy = emitter.abort; 27 | 28 | emitter.on('response', (response: Response) => { 29 | const {statusCode} = response; 30 | 31 | response.on('error', error => { 32 | proxy.emit('error', new ReadError(error, options)); 33 | }); 34 | 35 | if (options.throwHttpErrors && statusCode !== 304 && (statusCode < 200 || statusCode > 299)) { 36 | proxy.emit('error', new HTTPError(response, options), null, response); 37 | return; 38 | } 39 | 40 | isFinished = true; 41 | 42 | response.pipe(output); 43 | 44 | for (const destination of piped) { 45 | if (destination.headersSent) { 46 | continue; 47 | } 48 | 49 | for (const [key, value] of Object.entries(response.headers)) { 50 | // Got gives *decompressed* data. Overriding `content-encoding` header would result in an error. 51 | // It's not possible to decompress already decompressed data, is it? 52 | const allowed = options.decompress ? key !== 'content-encoding' : true; 53 | if (allowed) { 54 | destination.setHeader(key, value); 55 | } 56 | } 57 | 58 | destination.statusCode = response.statusCode; 59 | } 60 | 61 | proxy.emit('response', response); 62 | }); 63 | 64 | [ 65 | 'error', 66 | 'request', 67 | 'redirect', 68 | 'uploadProgress', 69 | 'downloadProgress' 70 | ].forEach(event => emitter.on(event, (...args) => proxy.emit(event, ...args))); 71 | 72 | const pipe = proxy.pipe.bind(proxy); 73 | const unpipe = proxy.unpipe.bind(proxy); 74 | proxy.pipe = (destination, options) => { 75 | if (isFinished) { 76 | throw new Error('Failed to pipe. The response has been emitted already.'); 77 | } 78 | 79 | pipe(destination, options); 80 | 81 | if (Reflect.has(destination, 'setHeader')) { 82 | piped.add(destination); 83 | } 84 | 85 | return destination; 86 | }; 87 | 88 | proxy.unpipe = stream => { 89 | piped.delete(stream); 90 | return unpipe(stream); 91 | }; 92 | 93 | return proxy; 94 | } 95 | -------------------------------------------------------------------------------- /source/utils/types.ts: -------------------------------------------------------------------------------- 1 | import {IncomingMessage} from 'http'; 2 | import {RequestOptions} from 'https'; 3 | import {Readable as ReadableStream} from 'stream'; 4 | import PCancelable from 'p-cancelable'; 5 | import {Hooks} from '../known-hook-events'; 6 | 7 | export type Method = 'GET' | 'PUT' | 'HEAD' | 'DELETE' | 'OPTIONS' | 'TRACE' | 'get' | 'put' | 'head' | 'delete' | 'options' | 'trace'; 8 | export type ErrorCode = 'ETIMEDOUT' | 'ECONNRESET' | 'EADDRINUSE' | 'ECONNREFUSED' | 'EPIPE' | 'ENOTFOUND' | 'ENETUNREACH' | 'EAI_AGAIN'; 9 | export type StatusCode = 408 | 413 | 429 | 500 | 502 | 503 | 504; 10 | 11 | export type NextFunction = (error?: Error | string) => void; 12 | 13 | export type IterateFunction = (options: Options) => void; 14 | 15 | export interface Response extends IncomingMessage { 16 | body: string | Buffer; 17 | statusCode: number; 18 | } 19 | 20 | export interface Timings { 21 | start: number; 22 | socket: number | null; 23 | lookup: number | null; 24 | connect: number | null; 25 | upload: number | null; 26 | response: number | null; 27 | end: number | null; 28 | error: number | null; 29 | phases: { 30 | wait: number | null; 31 | dns: number | null; 32 | tcp: number | null; 33 | request: number | null; 34 | firstByte: number | null; 35 | download: number | null; 36 | total: number | null; 37 | }; 38 | } 39 | 40 | export interface Instance { 41 | methods: Method[]; 42 | options: Partial; 43 | handler: (options: Options, callback: NextFunction) => void; 44 | } 45 | 46 | export interface InterfaceWithDefaults extends Instance { 47 | defaults: { 48 | handler: (options: Options, callback: NextFunction | IterateFunction) => void; 49 | options: Options; 50 | }; 51 | } 52 | 53 | interface RetryOption { 54 | retries?: ((retry: number, error: Error) => number) | number; 55 | methods?: Method[]; 56 | statusCodes?: StatusCode[]; 57 | maxRetryAfter?: number; 58 | errorCodes?: ErrorCode[]; 59 | } 60 | 61 | export interface MergedOptions extends Options { 62 | retry: RetryOption; 63 | } 64 | 65 | export interface Options extends RequestOptions { 66 | host: string; 67 | body: string | Buffer | ReadableStream; 68 | hostname?: string; 69 | path?: string; 70 | socketPath?: string; 71 | protocol?: string; 72 | href?: string; 73 | options?: Partial; 74 | hooks?: Partial; 75 | decompress?: boolean; 76 | encoding?: BufferEncoding | null; 77 | method?: Method; 78 | retry?: number | RetryOption; 79 | throwHttpErrors?: boolean; 80 | // TODO: Remove this once TS migration is complete and all options are defined. 81 | [key: string]: unknown; 82 | } 83 | 84 | export interface CancelableRequest extends PCancelable { 85 | on(name: string, listener: () => void): CancelableRequest; 86 | json(): CancelableRequest; 87 | buffer(): CancelableRequest; 88 | text(): CancelableRequest; 89 | } 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "got", 3 | "version": "9.6.0", 4 | "description": "Simplified HTTP requests", 5 | "license": "MIT", 6 | "repository": "sindresorhus/got", 7 | "main": "dist", 8 | "engines": { 9 | "node": ">=8.6" 10 | }, 11 | "scripts": { 12 | "test": "xo && nyc ava", 13 | "release": "np", 14 | "build": "del-cli dist && tsc", 15 | "prepublishOnly": "npm run build" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "keywords": [ 21 | "http", 22 | "https", 23 | "get", 24 | "got", 25 | "url", 26 | "uri", 27 | "request", 28 | "util", 29 | "utility", 30 | "simple", 31 | "curl", 32 | "wget", 33 | "fetch", 34 | "net", 35 | "network", 36 | "electron", 37 | "brotli" 38 | ], 39 | "dependencies": { 40 | "@sindresorhus/is": "^0.15.0", 41 | "@szmarczak/http-timer": "^1.1.2", 42 | "@types/form-data": "^2.2.1", 43 | "cacheable-lookup": "^0.1.0", 44 | "cacheable-request": "^6.0.0", 45 | "debug": "^4.1.1", 46 | "decompress-response": "^4.1.0", 47 | "duplexer3": "^0.1.4", 48 | "get-stream": "^5.0.0", 49 | "lowercase-keys": "^1.0.1", 50 | "mimic-response": "^2.0.0", 51 | "p-cancelable": "^1.1.0", 52 | "to-readable-stream": "^2.0.0" 53 | }, 54 | "devDependencies": { 55 | "@sindresorhus/tsconfig": "^0.3.0", 56 | "@types/duplexer3": "^0.1.0", 57 | "@types/node": "^11.11.0", 58 | "@typescript-eslint/eslint-plugin": "^1.5.0", 59 | "ava": "^1.3.1", 60 | "coveralls": "^3.0.0", 61 | "create-test-server": "^2.4.0", 62 | "del-cli": "^1.1.0", 63 | "delay": "^4.1.0", 64 | "eslint-config-xo-typescript": "^0.9.0", 65 | "form-data": "^2.3.3", 66 | "get-port": "^4.0.0", 67 | "nock": "^10.0.6", 68 | "np": "^4.0.2", 69 | "nyc": "^13.1.0", 70 | "p-event": "^4.0.0", 71 | "pem": "^1.14.1", 72 | "proxyquire": "^2.0.1", 73 | "sinon": "^7.2.2", 74 | "slow-stream": "0.0.4", 75 | "tempfile": "^2.0.0", 76 | "tempy": "^0.2.1", 77 | "tough-cookie": "^3.0.0", 78 | "ts-node": "^8.0.3", 79 | "typescript": "^3.3.3", 80 | "xo": "^0.24.0" 81 | }, 82 | "types": "dist", 83 | "browser": { 84 | "decompress-response": false, 85 | "electron": false 86 | }, 87 | "ava": { 88 | "concurrency": 4, 89 | "timeout": "1m", 90 | "babel": false, 91 | "compileEnhancements": false, 92 | "extensions": [ 93 | "ts" 94 | ], 95 | "require": [ 96 | "ts-node/register" 97 | ] 98 | }, 99 | "nyc": { 100 | "extension": [ 101 | ".ts" 102 | ] 103 | }, 104 | "xo": { 105 | "extends": "xo-typescript", 106 | "extensions": [ 107 | "ts" 108 | ], 109 | "rules": { 110 | "import/named": "off", 111 | "import/no-unresolved": "off", 112 | "ava/no-ignored-test-files": "off", 113 | "@typescript-eslint/explicit-function-return-type": "off", 114 | "@typescript-eslint/no-unnecessary-type-assertion": "off", 115 | "@typescript-eslint/ban-types": "off" 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /source/progress.ts: -------------------------------------------------------------------------------- 1 | import {Transform as TransformStream} from 'stream'; 2 | import {EventEmitter} from 'events'; 3 | import {IncomingMessage, ClientRequest} from 'http'; 4 | import {Socket} from 'net'; 5 | 6 | export function downloadProgress(_response: IncomingMessage, emitter: EventEmitter, downloadBodySize?: number): TransformStream { 7 | let downloaded = 0; 8 | 9 | return new TransformStream({ 10 | transform(chunk, _encoding, callback) { 11 | downloaded += chunk.length; 12 | 13 | const percent = downloadBodySize ? downloaded / downloadBodySize : 0; 14 | 15 | // Let `flush()` be responsible for emitting the last event 16 | if (percent < 1) { 17 | emitter.emit('downloadProgress', { 18 | percent, 19 | transferred: downloaded, 20 | total: downloadBodySize 21 | }); 22 | } 23 | 24 | callback(undefined, chunk); 25 | }, 26 | 27 | flush(callback) { 28 | emitter.emit('downloadProgress', { 29 | percent: 1, 30 | transferred: downloaded, 31 | total: downloadBodySize 32 | }); 33 | 34 | callback(); 35 | } 36 | }); 37 | } 38 | 39 | export function uploadProgress(request: ClientRequest, emitter: EventEmitter, uploadBodySize?: number): void { 40 | const uploadEventFrequency = 150; 41 | let uploaded = 0; 42 | let progressInterval: NodeJS.Timeout; 43 | 44 | emitter.emit('uploadProgress', { 45 | percent: 0, 46 | transferred: 0, 47 | total: uploadBodySize 48 | }); 49 | 50 | request.once('error', () => { 51 | clearInterval(progressInterval); 52 | }); 53 | 54 | request.once('response', () => { 55 | clearInterval(progressInterval); 56 | 57 | emitter.emit('uploadProgress', { 58 | percent: 1, 59 | transferred: uploaded, 60 | total: uploadBodySize 61 | }); 62 | }); 63 | 64 | request.once('socket', (socket: Socket) => { 65 | const onSocketConnect = (): void => { 66 | progressInterval = setInterval(() => { 67 | const lastUploaded = uploaded; 68 | /* istanbul ignore next: see #490 (occurs randomly!) */ 69 | const headersSize = (request as any)._header ? Buffer.byteLength((request as any)._header) : 0; 70 | uploaded = socket.bytesWritten - headersSize; 71 | 72 | // Don't emit events with unchanged progress and 73 | // prevent last event from being emitted, because 74 | // it's emitted when `response` is emitted 75 | if (uploaded === lastUploaded || uploaded === uploadBodySize) { 76 | return; 77 | } 78 | 79 | emitter.emit('uploadProgress', { 80 | percent: uploadBodySize ? uploaded / uploadBodySize : 0, 81 | transferred: uploaded, 82 | total: uploadBodySize 83 | }); 84 | }, uploadEventFrequency); 85 | }; 86 | 87 | /* istanbul ignore next: hard to test */ 88 | if (socket.connecting) { 89 | socket.once('connect', onSocketConnect); 90 | } else if (socket.writable) { 91 | // The socket is being reused from pool, 92 | // so the connect event will not be emitted 93 | onSocketConnect(); 94 | } 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /source/merge.ts: -------------------------------------------------------------------------------- 1 | import {URL, URLSearchParams} from 'url'; 2 | import is from '@sindresorhus/is'; 3 | import {Options, Method, NextFunction, Instance, InterfaceWithDefaults} from './utils/types'; 4 | import knownHookEvents, {Hooks, HookType, HookEvent} from './known-hook-events'; 5 | 6 | export default function merge(target: Target, ...sources: Source[]): Target & Source { 7 | for (const source of sources) { 8 | for (const [key, sourceValue] of Object.entries(source)) { 9 | if (is.undefined(sourceValue)) { 10 | continue; 11 | } 12 | 13 | const targetValue = target[key]; 14 | if (targetValue instanceof URLSearchParams && sourceValue instanceof URLSearchParams) { 15 | const params = new URLSearchParams(); 16 | 17 | const append = (value: string, key: string) => params.append(key, value); 18 | targetValue.forEach(append); 19 | sourceValue.forEach(append); 20 | 21 | target[key] = params; 22 | } else if (is.urlInstance(targetValue) && (is.urlInstance(sourceValue) || is.string(sourceValue))) { 23 | target[key] = new URL(sourceValue as string, targetValue); 24 | } else if (is.plainObject(sourceValue)) { 25 | if (is.plainObject(targetValue)) { 26 | target[key] = merge({}, targetValue, sourceValue); 27 | } else { 28 | target[key] = merge({}, sourceValue); 29 | } 30 | } else if (is.array(sourceValue)) { 31 | target[key] = merge([], sourceValue); 32 | } else { 33 | target[key] = sourceValue; 34 | } 35 | } 36 | } 37 | 38 | return target as Target & Source; 39 | } 40 | 41 | export function mergeOptions(...sources: Partial[]): Partial & {hooks: Partial} { 42 | sources = sources.map(source => source || {}); 43 | const merged = merge({}, ...sources); 44 | 45 | // TODO: This is a funky situation. Even though we "know" that we're going to 46 | // populate the `hooks` object in the loop below, TypeScript want us to 47 | // put them into the object upon initialization, because it cannot infer 48 | // that they are going to conform correctly in runtime. 49 | // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion 50 | const hooks = {} as {[Key in HookEvent]: HookType[]}; 51 | for (const hook of knownHookEvents) { 52 | hooks[hook] = []; 53 | } 54 | 55 | for (const source of sources) { 56 | if (source.hooks) { 57 | for (const hook of knownHookEvents) { 58 | hooks[hook] = hooks[hook].concat(source.hooks[hook] || []); 59 | } 60 | } 61 | } 62 | 63 | merged.hooks = hooks as Hooks; 64 | 65 | return merged as Partial & {hooks: Partial}; 66 | } 67 | 68 | export function mergeInstances(instances: InterfaceWithDefaults[], methods?: Method[]): Instance { 69 | const handlers = instances.map(instance => instance.defaults.handler); 70 | const size = instances.length - 1; 71 | 72 | return { 73 | methods, 74 | options: mergeOptions(...instances.map(instance => instance.defaults.options)), 75 | handler: (options: Options, next: NextFunction) => { 76 | let iteration = -1; 77 | const iterate = (options: Options): void => handlers[++iteration](options, iteration === size ? next : iterate); 78 | 79 | return iterate(options); 80 | } 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /test/gzip.ts: -------------------------------------------------------------------------------- 1 | import {promisify} from 'util'; 2 | import zlib from 'zlib'; 3 | import test from 'ava'; 4 | import getStream from 'get-stream'; 5 | import got from '../source'; 6 | import {createServer} from './helpers/server'; 7 | 8 | const testContent = 'Compressible response content.\n'; 9 | const testContentUncompressed = 'Uncompressed response content.\n'; 10 | 11 | let s; 12 | let gzipData; 13 | 14 | test.before('setup', async () => { 15 | s = await createServer(); 16 | gzipData = await promisify(zlib.gzip)(testContent); 17 | 18 | s.on('/', (request, response) => { 19 | response.statusCode = 200; 20 | response.setHeader('Content-Type', 'text/plain'); 21 | response.setHeader('Content-Encoding', 'gzip'); 22 | 23 | if (request.method === 'HEAD') { 24 | response.end(); 25 | return; 26 | } 27 | 28 | response.end(gzipData); 29 | }); 30 | 31 | s.on('/corrupted', (request, response) => { 32 | response.statusCode = 200; 33 | response.setHeader('Content-Type', 'text/plain'); 34 | response.setHeader('Content-Encoding', 'gzip'); 35 | response.end('Not gzipped content'); 36 | }); 37 | 38 | s.on('/missing-data', (request, response) => { 39 | response.statusCode = 200; 40 | response.setHeader('Content-Type', 'text/plain'); 41 | response.setHeader('Content-Encoding', 'gzip'); 42 | response.end(gzipData.slice(0, -1)); 43 | }); 44 | 45 | s.on('/uncompressed', (request, response) => { 46 | response.statusCode = 200; 47 | response.setHeader('Content-Type', 'text/plain'); 48 | response.end(testContentUncompressed); 49 | }); 50 | 51 | await s.listen(s.port); 52 | }); 53 | 54 | test.after('cleanup', async () => { 55 | await s.close(); 56 | }); 57 | 58 | test('decompress content', async t => { 59 | t.is((await got(s.url)).body, testContent); 60 | }); 61 | 62 | test('decompress content - stream', async t => { 63 | t.is(await getStream(got.stream(s.url)), testContent); 64 | }); 65 | 66 | test('handles gzip error', async t => { 67 | const error = await t.throwsAsync(got(`${s.url}/corrupted`)); 68 | t.is(error.message, 'incorrect header check'); 69 | // @ts-ignore 70 | t.is(error.path, '/corrupted'); 71 | t.is(error.name, 'ReadError'); 72 | }); 73 | 74 | test('handles gzip error - stream', async t => { 75 | const error = await t.throwsAsync(getStream(got.stream(`${s.url}/corrupted`))); 76 | t.is(error.message, 'incorrect header check'); 77 | // @ts-ignore 78 | t.is(error.path, '/corrupted'); 79 | t.is(error.name, 'ReadError'); 80 | }); 81 | 82 | test('decompress option opts out of decompressing', async t => { 83 | const {body} = await got(s.url, {decompress: false}); 84 | t.is(Buffer.compare(body, gzipData), 0); 85 | }); 86 | 87 | test('decompress option doesn\'t alter encoding of uncompressed responses', async t => { 88 | const {body} = await got(`${s.url}/uncompressed`, {decompress: false}); 89 | t.is(body, testContentUncompressed); 90 | }); 91 | 92 | test('preserve headers property', async t => { 93 | t.truthy((await got(s.url)).headers); 94 | }); 95 | 96 | test('do not break HEAD responses', async t => { 97 | t.is((await got.head(s.url)).body, ''); 98 | }); 99 | 100 | test('ignore missing data', async t => { 101 | t.is((await got(`${s.url}/missing-data`)).body, testContent); 102 | }); 103 | 104 | test('has url and requestUrl properties', async t => { 105 | const response = await got(s.url); 106 | t.truthy(response.url); 107 | t.truthy(response.requestUrl); 108 | }); 109 | -------------------------------------------------------------------------------- /test/url-to-options.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-deprecated-api */ 2 | import url from 'url'; 3 | import test from 'ava'; 4 | import urlToOptions from '../source/utils/url-to-options'; 5 | 6 | test('converts node legacy URL to options', t => { 7 | const exampleURL = 'https://user:password@github.com:443/say?hello=world#bang'; 8 | const parsedURL = url.parse(exampleURL); 9 | const options = urlToOptions(parsedURL); 10 | const expected = { 11 | hash: '#bang', 12 | host: 'github.com:443', 13 | hostname: 'github.com', 14 | href: exampleURL, 15 | path: '/say?hello=world', 16 | pathname: '/say', 17 | port: 443, 18 | protocol: 'https:', 19 | search: '?hello=world' 20 | }; 21 | 22 | t.deepEqual(options, expected); 23 | }); 24 | 25 | test('converts URL to options', t => { 26 | const exampleURL = 'https://user:password@github.com:443/say?hello=world#bang'; 27 | // TODO: Use the `URL` global when targeting Node.js 10 28 | const parsedURL = new url.URL(exampleURL); 29 | const options = urlToOptions(parsedURL); 30 | const expected = { 31 | auth: 'user:password', 32 | hash: '#bang', 33 | host: 'github.com', 34 | hostname: 'github.com', 35 | href: 'https://user:password@github.com/say?hello=world#bang', 36 | path: '/say?hello=world', 37 | pathname: '/say', 38 | protocol: 'https:', 39 | search: '?hello=world' 40 | }; 41 | 42 | t.deepEqual(options, expected); 43 | }); 44 | 45 | test('converts IPv6 URL to options', t => { 46 | const IPv6URL = 'https://[2001:cdba::3257:9652]:443/'; 47 | // TODO: Use the `URL` global when targeting Node.js 10 48 | const parsedURL = new url.URL(IPv6URL); 49 | const options = urlToOptions(parsedURL); 50 | const expected = { 51 | hash: '', 52 | host: '[2001:cdba::3257:9652]', 53 | hostname: '2001:cdba::3257:9652', 54 | href: 'https://[2001:cdba::3257:9652]/', 55 | path: '/', 56 | pathname: '/', 57 | protocol: 'https:', 58 | search: '' 59 | }; 60 | 61 | t.deepEqual(options, expected); 62 | }); 63 | 64 | test('only adds port to options for URLs with ports', t => { 65 | const noPortURL = 'https://github.com/'; 66 | const parsedURL = new url.URL(noPortURL); 67 | const options = urlToOptions(parsedURL); 68 | const expected = { 69 | hash: '', 70 | host: 'github.com', 71 | hostname: 'github.com', 72 | href: 'https://github.com/', 73 | path: '/', 74 | pathname: '/', 75 | protocol: 'https:', 76 | search: '' 77 | }; 78 | 79 | t.deepEqual(options, expected); 80 | t.false(Reflect.has(options, 'port')); 81 | }); 82 | 83 | test('does not concat null search to path', t => { 84 | const exampleURL = 'https://github.com/'; 85 | const parsedURL = url.parse(exampleURL); 86 | 87 | t.is(parsedURL.search, null); 88 | 89 | const options = urlToOptions(parsedURL); 90 | const expected = { 91 | hash: null, 92 | host: 'github.com', 93 | hostname: 'github.com', 94 | href: 'https://github.com/', 95 | path: '/', 96 | pathname: '/', 97 | protocol: 'https:', 98 | search: null 99 | }; 100 | 101 | t.deepEqual(options, expected); 102 | }); 103 | 104 | test('does not add null port to options', t => { 105 | const exampleURL = 'https://github.com/'; 106 | const parsedURL = url.parse(exampleURL); 107 | 108 | t.is(parsedURL.port, null); 109 | 110 | const options = urlToOptions(parsedURL); 111 | const expected = { 112 | hash: null, 113 | host: 'github.com', 114 | hostname: 'github.com', 115 | href: 'https://github.com/', 116 | path: '/', 117 | pathname: '/', 118 | protocol: 'https:', 119 | search: null 120 | }; 121 | 122 | t.deepEqual(options, expected); 123 | }); 124 | -------------------------------------------------------------------------------- /test/cookies.ts: -------------------------------------------------------------------------------- 1 | import net from 'net'; 2 | import test from 'ava'; 3 | import tough from 'tough-cookie'; 4 | import delay from 'delay'; 5 | import got from '../source'; 6 | import {createServer} from './helpers/server'; 7 | 8 | let s; 9 | 10 | test.before('setup', async () => { 11 | s = await createServer(); 12 | 13 | s.on('/set-cookie', (request, response) => { 14 | response.setHeader('set-cookie', 'hello=world'); 15 | response.end(); 16 | }); 17 | 18 | s.on('/set-multiple-cookies', (request, response) => { 19 | response.setHeader('set-cookie', ['hello=world', 'foo=bar']); 20 | response.end(); 21 | }); 22 | 23 | s.on('/set-cookies-then-redirect', (request, response) => { 24 | response.setHeader('set-cookie', ['hello=world', 'foo=bar']); 25 | response.setHeader('location', '/'); 26 | response.statusCode = 302; 27 | response.end(); 28 | }); 29 | 30 | s.on('/invalid', (request, response) => { 31 | response.setHeader('set-cookie', 'hello=world; domain=localhost'); 32 | response.end(); 33 | }); 34 | 35 | s.on('/', (request, response) => { 36 | response.end(request.headers.cookie || ''); 37 | }); 38 | 39 | await s.listen(s.port); 40 | }); 41 | 42 | test.after('cleanup', async () => { 43 | await s.close(); 44 | }); 45 | 46 | test('reads a cookie', async t => { 47 | const cookieJar = new tough.CookieJar(); 48 | 49 | await got(`${s.url}/set-cookie`, {cookieJar}); 50 | 51 | const cookie = cookieJar.getCookiesSync(s.url)[0]; 52 | t.is(cookie.key, 'hello'); 53 | t.is(cookie.value, 'world'); 54 | }); 55 | 56 | test('reads multiple cookies', async t => { 57 | const cookieJar = new tough.CookieJar(); 58 | 59 | await got(`${s.url}/set-multiple-cookies`, {cookieJar}); 60 | 61 | const cookies = cookieJar.getCookiesSync(s.url); 62 | const cookieA = cookies[0]; 63 | t.is(cookieA.key, 'hello'); 64 | t.is(cookieA.value, 'world'); 65 | 66 | const cookieB = cookies[1]; 67 | t.is(cookieB.key, 'foo'); 68 | t.is(cookieB.value, 'bar'); 69 | }); 70 | 71 | test('cookies doesn\'t break on redirects', async t => { 72 | const cookieJar = new tough.CookieJar(); 73 | 74 | const {body} = await got(`${s.url}/set-cookies-then-redirect`, {cookieJar}); 75 | t.is(body, 'hello=world; foo=bar'); 76 | }); 77 | 78 | test('throws on invalid cookies', async t => { 79 | const cookieJar = new tough.CookieJar(); 80 | 81 | await t.throwsAsync(() => got(`${s.url}/invalid`, {cookieJar}), 'Cookie has domain set to a public suffix'); 82 | }); 83 | 84 | test('catches store errors', async t => { 85 | const error = 'Some error'; 86 | const cookieJar = new tough.CookieJar({ 87 | findCookies: (_, __, cb) => { 88 | cb(new Error(error)); 89 | } 90 | }); 91 | 92 | await t.throwsAsync(() => got(s.url, {cookieJar}), error); 93 | }); 94 | 95 | test('overrides options.headers.cookie', async t => { 96 | const cookieJar = new tough.CookieJar(); 97 | const {body} = await got(`${s.url}/set-cookies-then-redirect`, { 98 | cookieJar, 99 | headers: { 100 | cookie: 'a=b' 101 | } 102 | }); 103 | t.is(body, 'hello=world; foo=bar'); 104 | }); 105 | 106 | test('no unhandled errors', async t => { 107 | const server = net.createServer(connection => { 108 | connection.end('blah'); 109 | }).listen(0); 110 | 111 | const message = 'snap!'; 112 | 113 | const options = { 114 | cookieJar: { 115 | setCookie: () => {}, 116 | getCookieString: (_, __, cb) => cb(new Error(message)) 117 | } 118 | }; 119 | 120 | // @ts-ignore 121 | await t.throwsAsync(got(`http://127.0.0.1:${server.address().port}`, options), {message}); 122 | await delay(500); 123 | t.pass(); 124 | 125 | server.close(); 126 | }); 127 | -------------------------------------------------------------------------------- /test/http.ts: -------------------------------------------------------------------------------- 1 | import is from '@sindresorhus/is'; 2 | import test from 'ava'; 3 | import got from '../source'; 4 | import withServer from './helpers/with-server'; 5 | 6 | test('simple request', withServer, async (t, s) => { 7 | s.get('/', (request, response) => { 8 | response.end('ok'); 9 | }); 10 | t.is((await got(s.url)).body, 'ok'); 11 | }); 12 | 13 | test('empty response', withServer, async (t, s) => { 14 | s.get('/empty', (request, response) => { 15 | response.end(); 16 | }); 17 | t.is((await got(`${s.url}/empty`)).body, ''); 18 | }); 19 | 20 | test('requestUrl response', withServer, async (t, s) => { 21 | s.get('/', (request, response) => { 22 | response.end('ok'); 23 | }); 24 | s.get('/empty', (request, response) => { 25 | response.end(); 26 | }); 27 | t.is((await got(s.url)).requestUrl, `${s.url}/`); 28 | t.is((await got(`${s.url}/empty`)).requestUrl, `${s.url}/empty`); 29 | }); 30 | 31 | test('error with code', withServer, async (t, s) => { 32 | s.get('/404', (request, response) => { 33 | response.statusCode = 404; 34 | response.end('not'); 35 | }); 36 | const error = await t.throwsAsync(got(`${s.url}/404`)); 37 | t.is(error.statusCode, 404); 38 | t.is(error.response.body, 'not'); 39 | }); 40 | 41 | test('status code 304 doesn\'t throw', withServer, async (t, s) => { 42 | s.get('/304', (request, response) => { 43 | response.statusCode = 304; 44 | response.end(); 45 | }); 46 | const promise = got(`${s.url}/304`); 47 | await t.notThrowsAsync(promise); 48 | const {statusCode, body} = await promise; 49 | t.is(statusCode, 304); 50 | t.is(body, ''); 51 | }); 52 | 53 | test('doesn\'t throw on throwHttpErrors === false', withServer, async (t, s) => { 54 | s.get('/404', (request, response) => { 55 | response.statusCode = 404; 56 | response.end('not'); 57 | }); 58 | t.is((await got(`${s.url}/404`, {throwHttpErrors: false})).body, 'not'); 59 | }); 60 | 61 | test('invalid protocol throws', async t => { 62 | const error = await t.throwsAsync(got('c:/nope.com').json()); 63 | t.is(error.constructor, got.UnsupportedProtocolError); 64 | }); 65 | 66 | test('buffer on encoding === null', withServer, async (t, s) => { 67 | s.get('/', (request, response) => { 68 | response.end('ok'); 69 | }); 70 | const data = (await got(s.url, {encoding: null})).body; 71 | t.true(is.buffer(data)); 72 | }); 73 | 74 | test('searchParams option', withServer, async (t, s) => { 75 | s.get('/', (request, response) => { 76 | response.end('recent'); 77 | }); 78 | s.get('/?recent=true', (request, response) => { 79 | response.end('recent'); 80 | }); 81 | t.is((await got(s.url, {searchParams: {recent: true}})).body, 'recent'); 82 | t.is((await got(s.url, {searchParams: 'recent=true'})).body, 'recent'); 83 | }); 84 | 85 | test('requestUrl response when sending url as param', withServer, async (t, s) => { 86 | s.get('/', (request, response) => { 87 | response.end('ok'); 88 | }); 89 | t.is((await got(s.url, {hostname: s.hostname, port: s.port})).requestUrl, `${s.url}/`); 90 | t.is((await got({hostname: s.hostname, port: s.port, protocol: 'http:'})).requestUrl, `${s.url}/`); 91 | }); 92 | 93 | test('response contains url', withServer, async (t, s) => { 94 | s.get('/', (request, response) => { 95 | response.end('ok'); 96 | }); 97 | t.is((await got(s.url)).url, `${s.url}/`); 98 | }); 99 | 100 | test('response contains got options', withServer, async (t, s) => { 101 | s.get('/', (request, response) => { 102 | response.end('ok'); 103 | }); 104 | 105 | const options = { 106 | url: s.url, 107 | auth: 'foo:bar' 108 | }; 109 | 110 | t.is((await got(options)).request.gotOptions.auth, options.auth); 111 | }); 112 | -------------------------------------------------------------------------------- /test/response-parse.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import got from '../source'; 3 | import {createServer} from './helpers/server'; 4 | 5 | let s; 6 | 7 | const jsonResponse = '{"data":"dog"}'; 8 | 9 | test.before('setup', async () => { 10 | s = await createServer(); 11 | 12 | s.on('/', (request, response) => { 13 | response.end(jsonResponse); 14 | }); 15 | 16 | s.on('/invalid', (request, response) => { 17 | response.end('/'); 18 | }); 19 | 20 | s.on('/no-body', (request, response) => { 21 | response.statusCode = 200; 22 | response.end(); 23 | }); 24 | 25 | s.on('/non200', (request, response) => { 26 | response.statusCode = 500; 27 | response.end(jsonResponse); 28 | }); 29 | 30 | s.on('/non200-invalid', (request, response) => { 31 | response.statusCode = 500; 32 | response.end('Internal error'); 33 | }); 34 | 35 | s.on('/headers', (request, response) => { 36 | response.end(JSON.stringify(request.headers)); 37 | }); 38 | 39 | await s.listen(s.port); 40 | }); 41 | 42 | test.after('cleanup', async () => { 43 | await s.close(); 44 | }); 45 | 46 | test('options.resolveBodyOnly works', async t => { 47 | t.deepEqual(await got(s.url, {responseType: 'json', resolveBodyOnly: true}), {data: 'dog'}); 48 | }); 49 | 50 | test('JSON response', async t => { 51 | t.deepEqual((await got(s.url, {responseType: 'json'})).body, {data: 'dog'}); 52 | }); 53 | 54 | test('Buffer response', async t => { 55 | t.deepEqual((await got(s.url, {responseType: 'buffer'})).body, Buffer.from(jsonResponse)); 56 | }); 57 | 58 | test('Text response', async t => { 59 | t.is((await got(s.url, {responseType: 'text'})).body, jsonResponse); 60 | }); 61 | 62 | test('JSON response - promise.json()', async t => { 63 | t.deepEqual(await got(s.url).json(), {data: 'dog'}); 64 | }); 65 | 66 | test('Buffer response - promise.buffer()', async t => { 67 | t.deepEqual(await got(s.url).buffer(), Buffer.from(jsonResponse)); 68 | }); 69 | 70 | test('Text response - promise.text()', async t => { 71 | t.is(await got(s.url).text(), jsonResponse); 72 | }); 73 | 74 | test('throws an error on invalid response type', async t => { 75 | await t.throwsAsync(() => got(s.url, {responseType: 'invalid'}), /^Failed to parse body of type 'invalid'/); 76 | }); 77 | 78 | test('doesn\'t parse responses without a body', async t => { 79 | const body = await got(`${s.url}/no-body`).json(); 80 | t.is(body, ''); 81 | }); 82 | 83 | test('wraps parsing errors', async t => { 84 | const error = await t.throwsAsync(got(`${s.url}/invalid`, {responseType: 'json'})); 85 | t.regex(error.message, /Unexpected token/); 86 | // @ts-ignore 87 | t.true(error.message.includes(error.hostname), error.message); 88 | // @ts-ignore 89 | t.is(error.path, '/invalid'); 90 | }); 91 | 92 | test('parses non-200 responses', async t => { 93 | const error = await t.throwsAsync(got(`${s.url}/non200`, {responseType: 'json'})); 94 | // @ts-ignore 95 | t.deepEqual(error.response.body, {data: 'dog'}); 96 | }); 97 | 98 | test('ignores errors on invalid non-200 responses', async t => { 99 | const error = await t.throwsAsync(got(`${s.url}/non200-invalid`, {responseType: 'json'})); 100 | t.is(error.message, 'Response code 500 (Internal Server Error)'); 101 | // @ts-ignore 102 | t.is(error.response.body, 'Internal error'); 103 | // @ts-ignore 104 | t.is(error.path, '/non200-invalid'); 105 | }); 106 | 107 | test('should have statusCode in error', async t => { 108 | const error = await t.throwsAsync(got(`${s.url}/invalid`, {responseType: 'json'})); 109 | t.is(error.constructor, got.ParseError); 110 | // @ts-ignore 111 | t.is(error.statusCode, 200); 112 | }); 113 | 114 | test('should set correct headers', async t => { 115 | const {body: headers} = await got.post(`${s.url}/headers`, {responseType: 'json', json: {}}); 116 | t.is(headers['content-type'], 'application/json'); 117 | t.is(headers.accept, 'application/json'); 118 | }); 119 | -------------------------------------------------------------------------------- /test/agent.ts: -------------------------------------------------------------------------------- 1 | import {Agent as HttpAgent} from 'http'; 2 | import {Agent as HttpsAgent} from 'https'; 3 | import test from 'ava'; 4 | import sinon from 'sinon'; 5 | import got from '../source'; 6 | import {createServer, createSSLServer} from './helpers/server'; 7 | 8 | let http; 9 | let https; 10 | 11 | test.before('setup', async () => { 12 | https = await createSSLServer(); 13 | http = await createServer(); 14 | 15 | // HTTPS Handlers 16 | 17 | https.on('/', (request, response) => { 18 | response.end('https'); 19 | }); 20 | 21 | https.on('/httpsToHttp', (request, response) => { 22 | response.writeHead(302, { 23 | location: http.url 24 | }); 25 | response.end(); 26 | }); 27 | 28 | // HTTP Handlers 29 | 30 | http.on('/', (request, response) => { 31 | response.end('http'); 32 | }); 33 | 34 | http.on('/httpToHttps', (request, response) => { 35 | response.writeHead(302, { 36 | location: https.url 37 | }); 38 | response.end(); 39 | }); 40 | 41 | await http.listen(http.port); 42 | await https.listen(https.port); 43 | }); 44 | 45 | test.after('cleanup', async () => { 46 | await http.close(); 47 | await https.close(); 48 | }); 49 | 50 | const createAgentSpy = Cls => { 51 | const agent = new Cls({keepAlive: true}); 52 | const spy = sinon.spy(agent, 'addRequest'); 53 | return {agent, spy}; 54 | }; 55 | 56 | test('non-object agent option works with http', async t => { 57 | const {agent, spy} = createAgentSpy(HttpAgent); 58 | 59 | t.truthy((await got(`${http.url}/`, { 60 | rejectUnauthorized: false, 61 | agent 62 | })).body); 63 | t.true(spy.calledOnce); 64 | 65 | // Make sure to close all open sockets 66 | agent.destroy(); 67 | }); 68 | 69 | test('non-object agent option works with https', async t => { 70 | const {agent, spy} = createAgentSpy(HttpsAgent); 71 | 72 | t.truthy((await got(`${https.url}/`, { 73 | rejectUnauthorized: false, 74 | agent 75 | })).body); 76 | t.true(spy.calledOnce); 77 | 78 | // Make sure to close all open sockets 79 | agent.destroy(); 80 | }); 81 | 82 | test('redirects from http to https work with an agent object', async t => { 83 | const {agent: httpAgent, spy: httpSpy} = createAgentSpy(HttpAgent); 84 | const {agent: httpsAgent, spy: httpsSpy} = createAgentSpy(HttpsAgent); 85 | 86 | t.truthy((await got(`${http.url}/httpToHttps`, { 87 | rejectUnauthorized: false, 88 | agent: { 89 | http: httpAgent, 90 | https: httpsAgent 91 | } 92 | })).body); 93 | t.true(httpSpy.calledOnce); 94 | t.true(httpsSpy.calledOnce); 95 | 96 | // Make sure to close all open sockets 97 | httpAgent.destroy(); 98 | httpsAgent.destroy(); 99 | }); 100 | 101 | test('redirects from https to http work with an agent object', async t => { 102 | const {agent: httpAgent, spy: httpSpy} = createAgentSpy(HttpAgent); 103 | const {agent: httpsAgent, spy: httpsSpy} = createAgentSpy(HttpsAgent); 104 | 105 | t.truthy((await got(`${https.url}/httpsToHttp`, { 106 | rejectUnauthorized: false, 107 | agent: { 108 | http: httpAgent, 109 | https: httpsAgent 110 | } 111 | })).body); 112 | t.true(httpSpy.calledOnce); 113 | t.true(httpsSpy.calledOnce); 114 | 115 | // Make sure to close all open sockets 116 | httpAgent.destroy(); 117 | httpsAgent.destroy(); 118 | }); 119 | 120 | test('socket connect listener cleaned up after request', async t => { 121 | const {agent} = createAgentSpy(HttpsAgent); 122 | 123 | // Make sure there are no memory leaks when reusing keep-alive sockets 124 | for (let i = 0; i < 20; i++) { 125 | // eslint-disable-next-line no-await-in-loop 126 | await got(`${https.url}`, { 127 | rejectUnauthorized: false, 128 | agent 129 | }); 130 | } 131 | 132 | for (const value of Object.values(agent.freeSockets) as [any]) { 133 | for (const sock of value) { 134 | t.is(sock.listenerCount('connect'), 0); 135 | } 136 | } 137 | 138 | // Make sure to close all open sockets 139 | agent.destroy(); 140 | }); 141 | -------------------------------------------------------------------------------- /source/errors.ts: -------------------------------------------------------------------------------- 1 | import urlLib from 'url'; 2 | import http, {IncomingHttpHeaders} from 'http'; 3 | import is from '@sindresorhus/is'; 4 | import {Response, Timings, Options} from './utils/types'; 5 | import {TimeoutError as TimedOutError} from './utils/timed-out'; 6 | 7 | type ErrorWithCode = (Error & {code?: string}) | {code?: string}; 8 | 9 | export class GotError extends Error { 10 | code?: string; 11 | 12 | constructor(message: string, error: ErrorWithCode, options: Options) { 13 | super(message); 14 | Error.captureStackTrace(this, this.constructor); 15 | this.name = 'GotError'; 16 | 17 | if (!is.undefined(error.code)) { 18 | this.code = error.code; 19 | } 20 | 21 | Object.assign(this, { 22 | host: options.host, 23 | hostname: options.hostname, 24 | method: options.method, 25 | path: options.path, 26 | socketPath: options.socketPath, 27 | protocol: options.protocol, 28 | url: options.href, 29 | gotOptions: options 30 | }); 31 | } 32 | } 33 | 34 | export class CacheError extends GotError { 35 | constructor(error: Error, options: Options) { 36 | super(error.message, error, options); 37 | this.name = 'CacheError'; 38 | } 39 | } 40 | 41 | export class RequestError extends GotError { 42 | constructor(error: Error, options: Options) { 43 | super(error.message, error, options); 44 | this.name = 'RequestError'; 45 | } 46 | } 47 | 48 | export class ReadError extends GotError { 49 | constructor(error: Error, options: Options) { 50 | super(error.message, error, options); 51 | this.name = 'ReadError'; 52 | } 53 | } 54 | 55 | export class ParseError extends GotError { 56 | body: string | Buffer; 57 | 58 | statusCode: number; 59 | 60 | statusMessage?: string; 61 | 62 | constructor(error: Error, statusCode: number, options: Options, data: string | Buffer) { 63 | super(`${error.message} in "${urlLib.format(options)}"`, error, options); 64 | this.name = 'ParseError'; 65 | this.body = data; 66 | this.statusCode = statusCode; 67 | this.statusMessage = http.STATUS_CODES[this.statusCode]; 68 | } 69 | } 70 | 71 | export class HTTPError extends GotError { 72 | headers?: IncomingHttpHeaders; 73 | 74 | body: string | Buffer; 75 | 76 | statusCode: number; 77 | 78 | statusMessage?: string; 79 | 80 | constructor(response: Response, options: Options) { 81 | const {statusCode} = response; 82 | let {statusMessage} = response; 83 | 84 | if (statusMessage) { 85 | statusMessage = statusMessage.replace(/\r?\n/g, ' ').trim(); 86 | } else { 87 | statusMessage = http.STATUS_CODES[statusCode]; 88 | } 89 | 90 | super(`Response code ${statusCode} (${statusMessage})`, {}, options); 91 | this.name = 'HTTPError'; 92 | this.statusCode = statusCode; 93 | this.statusMessage = statusMessage; 94 | this.headers = response.headers; 95 | this.body = response.body; 96 | } 97 | } 98 | 99 | export class MaxRedirectsError extends GotError { 100 | redirectUrls?: string[]; 101 | 102 | statusMessage?: string; 103 | 104 | statusCode: number; 105 | 106 | constructor(statusCode: number, redirectUrls: string[], options: Options) { 107 | super('Redirected 10 times. Aborting.', {}, options); 108 | this.name = 'MaxRedirectsError'; 109 | this.statusCode = statusCode; 110 | this.statusMessage = http.STATUS_CODES[this.statusCode]; 111 | this.redirectUrls = redirectUrls; 112 | } 113 | } 114 | 115 | export class UnsupportedProtocolError extends GotError { 116 | constructor(options: Options) { 117 | super(`Unsupported protocol "${options.protocol}"`, {}, options); 118 | this.name = 'UnsupportedProtocolError'; 119 | } 120 | } 121 | 122 | export class TimeoutError extends GotError { 123 | timings: Timings; 124 | 125 | event: string; 126 | 127 | constructor(error: TimedOutError, timings: Timings, options: Options) { 128 | super(error.message, {code: 'ETIMEDOUT'}, options); 129 | this.name = 'TimeoutError'; 130 | this.event = error.event; 131 | this.timings = timings; 132 | } 133 | } 134 | 135 | export {CancelError} from 'p-cancelable'; 136 | -------------------------------------------------------------------------------- /test/post.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import toReadableStream from 'to-readable-stream'; 3 | import got from '../source'; 4 | import {createServer} from './helpers/server'; 5 | 6 | let s; 7 | 8 | test.before('setup', async () => { 9 | s = await createServer(); 10 | 11 | s.on('/', (request, response) => { 12 | response.setHeader('method', request.method); 13 | request.pipe(response); 14 | }); 15 | 16 | s.on('/headers', (request, response) => { 17 | response.end(JSON.stringify(request.headers)); 18 | }); 19 | 20 | s.on('/empty', (request_, response) => { 21 | response.end(); 22 | }); 23 | 24 | await s.listen(s.port); 25 | }); 26 | 27 | test.after('cleanup', async () => { 28 | await s.close(); 29 | }); 30 | 31 | test('GET cannot have body', async t => { 32 | await t.throwsAsync(got.get(s.url, {body: 'hi'}), 'The `GET` method cannot be used with a body'); 33 | }); 34 | 35 | test('sends strings', async t => { 36 | const {body} = await got.post(s.url, {body: 'wow'}); 37 | t.is(body, 'wow'); 38 | }); 39 | 40 | test('sends Buffers', async t => { 41 | const {body} = await got.post(s.url, {body: Buffer.from('wow')}); 42 | t.is(body, 'wow'); 43 | }); 44 | 45 | test('sends Streams', async t => { 46 | const {body} = await got.post(s.url, {body: toReadableStream('wow')}); 47 | t.is(body, 'wow'); 48 | }); 49 | 50 | test('sends plain objects as forms', async t => { 51 | const {body} = await got.post(s.url, { 52 | form: {such: 'wow'} 53 | }); 54 | t.is(body, 'such=wow'); 55 | }); 56 | 57 | test('does NOT support sending arrays as forms', async t => { 58 | await t.throwsAsync(got.post(s.url, { 59 | form: ['such', 'wow'] 60 | }), TypeError); 61 | }); 62 | 63 | test('sends plain objects as JSON', async t => { 64 | const {body} = await got.post(s.url, { 65 | json: {such: 'wow'}, 66 | responseType: 'json' 67 | }); 68 | t.deepEqual(body, {such: 'wow'}); 69 | }); 70 | 71 | test('sends arrays as JSON', async t => { 72 | const {body} = await got.post(s.url, { 73 | json: ['such', 'wow'], 74 | responseType: 'json' 75 | }); 76 | t.deepEqual(body, ['such', 'wow']); 77 | }); 78 | 79 | test('works with empty post response', async t => { 80 | const {body} = await got.post(`${s.url}/empty`, {body: 'wow'}); 81 | t.is(body, ''); 82 | }); 83 | 84 | test('content-length header with string body', async t => { 85 | const {body} = await got.post(`${s.url}/headers`, {body: 'wow'}); 86 | const headers = JSON.parse(body); 87 | t.is(headers['content-length'], '3'); 88 | }); 89 | 90 | test('content-length header with Buffer body', async t => { 91 | const {body} = await got.post(`${s.url}/headers`, {body: Buffer.from('wow')}); 92 | const headers = JSON.parse(body); 93 | t.is(headers['content-length'], '3'); 94 | }); 95 | 96 | test('content-length header with Stream body', async t => { 97 | const {body} = await got.post(`${s.url}/headers`, {body: toReadableStream('wow')}); 98 | const headers = JSON.parse(body); 99 | t.is(headers['transfer-encoding'], 'chunked', 'likely failed to get headers at all'); 100 | t.is(headers['content-length'], undefined); 101 | }); 102 | 103 | test('content-length header is not overriden', async t => { 104 | const {body} = await got.post(`${s.url}/headers`, { 105 | body: 'wow', 106 | headers: { 107 | 'content-length': '10' 108 | } 109 | }); 110 | const headers = JSON.parse(body); 111 | t.is(headers['content-length'], '10'); 112 | }); 113 | 114 | test('content-length header disabled for chunked transfer-encoding', async t => { 115 | const {body} = await got.post(`${s.url}/headers`, { 116 | body: '3\r\nwow\r\n0\r\n', 117 | headers: { 118 | 'transfer-encoding': 'chunked' 119 | } 120 | }); 121 | const headers = JSON.parse(body); 122 | t.is(headers['transfer-encoding'], 'chunked', 'likely failed to get headers at all'); 123 | t.is(headers['content-length'], undefined); 124 | }); 125 | 126 | test('content-type header is not overriden when object in options.body', async t => { 127 | const {body: headers} = await got.post(`${s.url}/headers`, { 128 | headers: { 129 | 'content-type': 'doge' 130 | }, 131 | json: { 132 | such: 'wow' 133 | }, 134 | responseType: 'json' 135 | }); 136 | t.is(headers['content-type'], 'doge'); 137 | }); 138 | 139 | test('throws when form body is not a plain object or array', async t => { 140 | await t.throwsAsync(got.post(`${s.url}`, {form: 'such=wow'}), TypeError); 141 | }); 142 | -------------------------------------------------------------------------------- /test/cancel.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | import {Readable as ReadableStream} from 'stream'; 3 | import test from 'ava'; 4 | import pEvent from 'p-event'; 5 | // @ts-ignore 6 | import got, {CancelError} from '../source'; 7 | import {createServer} from './helpers/server'; 8 | 9 | async function createAbortServer() { 10 | const s = await createServer(); 11 | const ee = new EventEmitter(); 12 | // @ts-ignore 13 | ee.aborted = new Promise((resolve, reject) => { 14 | s.on('/abort', async (request, response) => { 15 | ee.emit('connection'); 16 | request.on('aborted', resolve); 17 | response.on('finish', reject.bind(null, new Error('Request finished instead of aborting.'))); 18 | 19 | await pEvent(request, 'end'); 20 | response.end(); 21 | }); 22 | 23 | s.on('/redirect', (request, response) => { 24 | response.writeHead(302, { 25 | location: `${s.url}/abort` 26 | }); 27 | response.end(); 28 | 29 | ee.emit('sentRedirect'); 30 | 31 | setTimeout(resolve, 3000); 32 | }); 33 | }); 34 | 35 | await s.listen(s.port); 36 | // @ts-ignore 37 | ee.url = `${s.url}/abort`; 38 | // @ts-ignore 39 | ee.redirectUrl = `${s.url}/redirect`; 40 | 41 | return ee; 42 | } 43 | 44 | test('cancel do not retry after cancelation', async t => { 45 | const helper = await createAbortServer(); 46 | 47 | // @ts-ignore 48 | const p = got(helper.redirectUrl, { 49 | retry: { 50 | retries: () => { 51 | t.fail('Makes a new try after cancelation'); 52 | } 53 | } 54 | }); 55 | 56 | helper.on('sentRedirect', () => { 57 | p.cancel(); 58 | }); 59 | 60 | // @ts-ignore 61 | await t.throwsAsync(p, CancelError); 62 | // @ts-ignore 63 | await t.notThrowsAsync(helper.aborted, 'Request finished instead of aborting.'); 64 | }); 65 | 66 | test('cancel in-progress request', async t => { 67 | const helper = await createAbortServer(); 68 | const body = new ReadableStream({ 69 | read() {} 70 | }); 71 | body.push('1'); 72 | 73 | // @ts-ignore 74 | const p = got.post(helper.url, {body}); 75 | 76 | // Wait for the connection to be established before canceling 77 | helper.on('connection', () => { 78 | p.cancel(); 79 | body.push(null); 80 | }); 81 | 82 | // @ts-ignore 83 | await t.throwsAsync(p, CancelError); 84 | // @ts-ignore 85 | await t.notThrowsAsync(helper.aborted, 'Request finished instead of aborting.'); 86 | }); 87 | 88 | test('cancel in-progress request with timeout', async t => { 89 | const helper = await createAbortServer(); 90 | const body = new ReadableStream({ 91 | read() {} 92 | }); 93 | body.push('1'); 94 | 95 | // @ts-ignore 96 | const p = got.post(helper.url, {body, timeout: 10000}); 97 | 98 | // Wait for the connection to be established before canceling 99 | helper.on('connection', () => { 100 | p.cancel(); 101 | body.push(null); 102 | }); 103 | 104 | await t.throwsAsync(p, CancelError); 105 | // @ts-ignore 106 | await t.notThrowsAsync(helper.aborted, 'Request finished instead of aborting.'); 107 | }); 108 | 109 | test('cancel immediately', async t => { 110 | const s = await createServer(); 111 | const aborted = new Promise((resolve, reject) => { 112 | // We won't get an abort or even a connection 113 | // We assume no request within 1000ms equals a (client side) aborted request 114 | s.on('/abort', (request, response) => { 115 | response.on('finish', reject.bind(this, new Error('Request finished instead of aborting.'))); 116 | response.end(); 117 | }); 118 | setTimeout(resolve, 1000); 119 | }); 120 | 121 | await s.listen(s.port); 122 | 123 | const p = got(`${s.url}/abort`); 124 | p.cancel(); 125 | await t.throwsAsync(p); 126 | await t.notThrowsAsync(aborted, 'Request finished instead of aborting.'); 127 | }); 128 | 129 | test('recover from cancelation using cancelable promise attribute', async t => { 130 | // Canceled before connection started 131 | const p = got('http://example.com'); 132 | const recover = p.catch(error => { 133 | if (p.isCanceled) { 134 | return; 135 | } 136 | 137 | throw error; 138 | }); 139 | 140 | p.cancel(); 141 | 142 | await t.notThrowsAsync(recover); 143 | }); 144 | 145 | test('recover from cancellation using error instance', async t => { 146 | // Canceled before connection started 147 | const p = got('http://example.com'); 148 | const recover = p.catch(error => { 149 | if (error instanceof got.CancelError) { 150 | return; 151 | } 152 | 153 | throw error; 154 | }); 155 | 156 | p.cancel(); 157 | 158 | await t.notThrowsAsync(recover); 159 | }); 160 | -------------------------------------------------------------------------------- /source/as-promise.ts: -------------------------------------------------------------------------------- 1 | import {ClientRequest, IncomingMessage} from 'http'; 2 | import EventEmitter from 'events'; 3 | import getStream from 'get-stream'; 4 | import is from '@sindresorhus/is'; 5 | import PCancelable from 'p-cancelable'; 6 | import requestAsEventEmitter from './request-as-event-emitter'; 7 | import {HTTPError, ParseError, ReadError} from './errors'; 8 | import {mergeOptions} from './merge'; 9 | import {reNormalizeArguments} from './normalize-arguments'; 10 | import {CancelableRequest, Options, Response} from './utils/types'; 11 | 12 | // TODO: Remove once request-as-event-emitter is converted to TypeScript 13 | interface RequestAsEventEmitter extends ClientRequest { 14 | retry: (error: Error) => boolean; 15 | } 16 | 17 | export default function asPromise(options: Options) { 18 | const proxy = new EventEmitter(); 19 | 20 | const parseBody = (response: Response) => { 21 | if (options.responseType === 'json') { 22 | response.body = JSON.parse(response.body as string); 23 | } else if (options.responseType === 'buffer') { 24 | response.body = Buffer.from(response.body as Buffer); 25 | } else if (options.responseType !== 'text' && !is.falsy(options.responseType)) { 26 | throw new Error(`Failed to parse body of type '${options.responseType}'`); 27 | } 28 | }; 29 | 30 | const promise = new PCancelable((resolve, reject, onCancel) => { 31 | const emitter = requestAsEventEmitter(options) as RequestAsEventEmitter; 32 | 33 | onCancel(emitter.abort); 34 | 35 | emitter.on('response', async response => { 36 | proxy.emit('response', response); 37 | 38 | const stream = is.null_(options.encoding) ? getStream.buffer(response) : getStream(response, {encoding: options.encoding}); 39 | 40 | let data; 41 | try { 42 | data = await stream; 43 | } catch (error) { 44 | reject(new ReadError(error, options)); 45 | return; 46 | } 47 | 48 | const limitStatusCode = options.followRedirect ? 299 : 399; 49 | 50 | response.body = data; 51 | 52 | try { 53 | for (const [index, hook] of options.hooks!.afterResponse!.entries()) { 54 | // eslint-disable-next-line no-await-in-loop 55 | response = await hook(response, updatedOptions => { 56 | updatedOptions = reNormalizeArguments(mergeOptions(options, { 57 | ...updatedOptions, 58 | retry: 0, 59 | throwHttpErrors: false 60 | })); 61 | 62 | // Remove any further hooks for that request, because we we'll call them anyway. 63 | // The loop continues. We don't want duplicates (asPromise recursion). 64 | updatedOptions.hooks!.afterResponse = options.hooks!.afterResponse!.slice(0, index); 65 | 66 | return asPromise(updatedOptions); 67 | }); 68 | } 69 | } catch (error) { 70 | reject(error); 71 | return; 72 | } 73 | 74 | const {statusCode} = response; 75 | 76 | if (response.body) { 77 | try { 78 | parseBody(response); 79 | } catch (error) { 80 | if (statusCode >= 200 && statusCode < 300) { 81 | const parseError = new ParseError(error, statusCode, options, data); 82 | Object.defineProperty(parseError, 'response', {value: response}); 83 | reject(parseError); 84 | return; 85 | } 86 | } 87 | } 88 | 89 | if (statusCode !== 304 && (statusCode < 200 || statusCode > limitStatusCode)) { 90 | const error = new HTTPError(response, options); 91 | Object.defineProperty(error, 'response', {value: response}); 92 | if (emitter.retry(error) === false) { 93 | if (options.throwHttpErrors) { 94 | reject(error); 95 | return; 96 | } 97 | 98 | resolve(options.resolveBodyOnly ? response.body : response); 99 | } 100 | 101 | return; 102 | } 103 | 104 | resolve(options.resolveBodyOnly ? response.body : response); 105 | }); 106 | 107 | emitter.once('error', reject); 108 | [ 109 | 'request', 110 | 'redirect', 111 | 'uploadProgress', 112 | 'downloadProgress' 113 | ].forEach(event => emitter.on(event, (...args) => proxy.emit(event, ...args))); 114 | }) as CancelableRequest; 115 | 116 | promise.on = (name: string, fn: () => void) => { 117 | proxy.on(name, fn); 118 | return promise; 119 | }; 120 | 121 | promise.json = () => { 122 | options.responseType = 'json'; 123 | options.resolveBodyOnly = true; 124 | return promise; 125 | }; 126 | 127 | promise.buffer = () => { 128 | options.responseType = 'buffer'; 129 | options.resolveBodyOnly = true; 130 | return promise; 131 | }; 132 | 133 | promise.text = () => { 134 | options.responseType = 'text'; 135 | options.resolveBodyOnly = true; 136 | return promise; 137 | }; 138 | 139 | return promise; 140 | } 141 | -------------------------------------------------------------------------------- /test/cache.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import got from '../source'; 3 | import {createServer} from './helpers/server'; 4 | 5 | let s; 6 | 7 | test.before('setup', async () => { 8 | s = await createServer(); 9 | 10 | let noStoreIndex = 0; 11 | s.on('/no-store', (request, response) => { 12 | response.setHeader('Cache-Control', 'public, no-cache, no-store'); 13 | response.end(noStoreIndex.toString()); 14 | noStoreIndex++; 15 | }); 16 | 17 | let cacheIndex = 0; 18 | s.on('/cache', (request, response) => { 19 | response.setHeader('Cache-Control', 'public, max-age=60'); 20 | response.end(cacheIndex.toString()); 21 | cacheIndex++; 22 | }); 23 | 24 | let calledFirstError = false; 25 | s.on('/first-error', (request, response) => { 26 | if (calledFirstError) { 27 | response.end('ok'); 28 | return; 29 | } 30 | 31 | calledFirstError = true; 32 | response.statusCode = 502; 33 | response.end('received 502'); 34 | }); 35 | 36 | let status301Index = 0; 37 | s.on('/301', (request, response) => { 38 | if (status301Index === 0) { 39 | response.setHeader('Cache-Control', 'public, max-age=60'); 40 | response.setHeader('Location', `${s.url}/302`); 41 | response.statusCode = 301; 42 | } 43 | 44 | response.end(); 45 | status301Index++; 46 | }); 47 | 48 | let status302Index = 0; 49 | s.on('/302', (request, response) => { 50 | if (status302Index === 0) { 51 | response.setHeader('Cache-Control', 'public, max-age=60'); 52 | response.setHeader('Location', `${s.url}/cache`); 53 | response.statusCode = 302; 54 | } 55 | 56 | response.end(); 57 | status302Index++; 58 | }); 59 | 60 | await s.listen(s.port); 61 | }); 62 | 63 | test.after('cleanup', async () => { 64 | await s.close(); 65 | }); 66 | 67 | test('Non cacheable responses are not cached', async t => { 68 | const endpoint = '/no-store'; 69 | const cache = new Map(); 70 | 71 | const firstResponseInt = Number((await got(`${s.url}${endpoint}`, {cache})).body); 72 | const secondResponseInt = Number((await got(`${s.url}${endpoint}`, {cache})).body); 73 | 74 | t.is(cache.size, 0); 75 | t.true(firstResponseInt < secondResponseInt); 76 | }); 77 | 78 | test('Cacheable responses are cached', async t => { 79 | const endpoint = '/cache'; 80 | const cache = new Map(); 81 | 82 | const firstResponse = await got(`${s.url}${endpoint}`, {cache}); 83 | const secondResponse = await got(`${s.url}${endpoint}`, {cache}); 84 | 85 | t.is(cache.size, 1); 86 | t.is(firstResponse.body, secondResponse.body); 87 | }); 88 | 89 | test('Cached response is re-encoded to current encoding option', async t => { 90 | const endpoint = '/cache'; 91 | const cache = new Map(); 92 | const firstEncoding = 'base64'; 93 | const secondEncoding = 'hex'; 94 | 95 | const firstResponse = await got(`${s.url}${endpoint}`, {cache, encoding: firstEncoding}); 96 | const secondResponse = await got(`${s.url}${endpoint}`, {cache, encoding: secondEncoding}); 97 | 98 | const expectedSecondResponseBody = Buffer.from(firstResponse.body, firstEncoding).toString(secondEncoding); 99 | 100 | t.is(cache.size, 1); 101 | t.is(secondResponse.body, expectedSecondResponseBody); 102 | }); 103 | 104 | test('Redirects are cached and re-used internally', async t => { 105 | const endpoint = '/301'; 106 | const cache = new Map(); 107 | 108 | const firstResponse = await got(`${s.url}${endpoint}`, {cache}); 109 | const secondResponse = await got(`${s.url}${endpoint}`, {cache}); 110 | 111 | t.is(cache.size, 3); 112 | t.is(firstResponse.body, secondResponse.body); 113 | }); 114 | 115 | test('Cached response should have got options', async t => { 116 | const endpoint = '/cache'; 117 | const cache = new Map(); 118 | const options = { 119 | url: `${s.url}${endpoint}`, 120 | auth: 'foo:bar', 121 | cache 122 | }; 123 | 124 | await got(options); 125 | const secondResponse = await got(options); 126 | 127 | t.is(secondResponse.request.gotOptions.auth, options.auth); 128 | }); 129 | 130 | test('Cache error throws got.CacheError', async t => { 131 | const endpoint = '/no-store'; 132 | const cache = {}; 133 | 134 | const error = await t.throwsAsync(got(`${s.url}${endpoint}`, {cache})); 135 | t.is(error.name, 'CacheError'); 136 | }); 137 | 138 | test('doesn\'t cache response when received HTTP error', async t => { 139 | const endpoint = '/first-error'; 140 | const cache = new Map(); 141 | 142 | const {statusCode, body} = await got(`${s.url}${endpoint}`, {cache, throwHttpErrors: false}); 143 | t.is(statusCode, 200); 144 | t.deepEqual(body, 'ok'); 145 | }); 146 | 147 | test('DNS cache works', async t => { 148 | const map = new Map(); 149 | await t.notThrowsAsync(got('https://example.com', {dnsCache: map})); 150 | 151 | t.is(map.size, 1); 152 | }); 153 | -------------------------------------------------------------------------------- /test/merge-instances.ts: -------------------------------------------------------------------------------- 1 | import {URLSearchParams} from 'url'; 2 | import test from 'ava'; 3 | import got from '../source'; 4 | import withServer from './helpers/with-server'; 5 | 6 | const responseFn = (request, response) => { 7 | request.resume(); 8 | response.end(JSON.stringify(request.headers)); 9 | }; 10 | 11 | test('merging instances', withServer, async (t, s) => { 12 | s.get('/', responseFn); 13 | 14 | const instanceA = got.extend({headers: {unicorn: 'rainbow'}}); 15 | const instanceB = got.extend({baseUrl: s.url}); 16 | const merged = got.mergeInstances(instanceA, instanceB); 17 | 18 | const headers = await merged('/').json(); 19 | t.is(headers.unicorn, 'rainbow'); 20 | t.not(headers['user-agent'], undefined); 21 | }); 22 | 23 | // TODO: Enable this test again. It currently throws an unhandled rejection. 24 | // test('works even if no default handler in the end', withServer, async (t, s) => { 25 | // s.get('/', responseFn); 26 | 27 | // const instanceA = got.create({ 28 | // options: {}, 29 | // handler: (options, next) => next(options) 30 | // }); 31 | 32 | // const instanceB = got.create({ 33 | // options: {}, 34 | // handler: (options, next) => next(options) 35 | // }); 36 | 37 | // const merged = got.mergeInstances(instanceA, instanceB); 38 | // await t.notThrows(() => merged(s.url)); 39 | // }); 40 | 41 | test('merges default handlers & custom handlers', withServer, async (t, s) => { 42 | s.get('/', responseFn); 43 | const instanceA = got.extend({headers: {unicorn: 'rainbow'}}); 44 | const instanceB = got.create({ 45 | options: {}, 46 | handler: (options, next) => { 47 | options.headers.cat = 'meow'; 48 | return next(options); 49 | } 50 | }); 51 | const merged = got.mergeInstances(instanceA, instanceB); 52 | 53 | const headers = await merged(s.url).json(); 54 | t.is(headers.unicorn, 'rainbow'); 55 | t.is(headers.cat, 'meow'); 56 | }); 57 | 58 | test('merging one group & one instance', withServer, async (t, s) => { 59 | s.get('/', responseFn); 60 | 61 | const instanceA = got.extend({headers: {dog: 'woof'}}); 62 | const instanceB = got.extend({headers: {cat: 'meow'}}); 63 | const instanceC = got.extend({headers: {bird: 'tweet'}}); 64 | const instanceD = got.extend({headers: {mouse: 'squeek'}}); 65 | 66 | const merged = got.mergeInstances(instanceA, instanceB, instanceC); 67 | const doubleMerged = got.mergeInstances(merged, instanceD); 68 | 69 | const headers = await doubleMerged(s.url).json(); 70 | t.is(headers.dog, 'woof'); 71 | t.is(headers.cat, 'meow'); 72 | t.is(headers.bird, 'tweet'); 73 | t.is(headers.mouse, 'squeek'); 74 | }); 75 | 76 | test('merging two groups of merged instances', withServer, async (t, s) => { 77 | s.get('/', responseFn); 78 | 79 | const instanceA = got.extend({headers: {dog: 'woof'}}); 80 | const instanceB = got.extend({headers: {cat: 'meow'}}); 81 | const instanceC = got.extend({headers: {bird: 'tweet'}}); 82 | const instanceD = got.extend({headers: {mouse: 'squeek'}}); 83 | 84 | const groupA = got.mergeInstances(instanceA, instanceB); 85 | const groupB = got.mergeInstances(instanceC, instanceD); 86 | 87 | const merged = got.mergeInstances(groupA, groupB); 88 | 89 | const headers = await merged(s.url).json(); 90 | t.is(headers.dog, 'woof'); 91 | t.is(headers.cat, 'meow'); 92 | t.is(headers.bird, 'tweet'); 93 | t.is(headers.mouse, 'squeek'); 94 | }); 95 | 96 | test('hooks are merged', t => { 97 | const getBeforeRequestHooks = instance => instance.defaults.options.hooks.beforeRequest; 98 | 99 | const instanceA = got.extend({hooks: { 100 | beforeRequest: [ 101 | options => { 102 | options.headers.dog = 'woof'; 103 | } 104 | ] 105 | }}); 106 | const instanceB = got.extend({hooks: { 107 | beforeRequest: [ 108 | options => { 109 | options.headers.cat = 'meow'; 110 | } 111 | ] 112 | }}); 113 | 114 | const merged = got.mergeInstances(instanceA, instanceB); 115 | t.deepEqual(getBeforeRequestHooks(merged), getBeforeRequestHooks(instanceA).concat(getBeforeRequestHooks(instanceB))); 116 | }); 117 | 118 | test('hooks are passed by though other instances don\'t have them', t => { 119 | const instanceA = got.extend({hooks: { 120 | beforeRequest: [ 121 | options => { 122 | options.headers.dog = 'woof'; 123 | } 124 | ] 125 | }}); 126 | const instanceB = got.create({ 127 | options: {} 128 | }); 129 | const instanceC = got.create({ 130 | options: {hooks: {}} 131 | }); 132 | 133 | const merged = got.mergeInstances(instanceA, instanceB, instanceC); 134 | t.deepEqual(merged.defaults.options.hooks.beforeRequest, instanceA.defaults.options.hooks.beforeRequest); 135 | }); 136 | 137 | test('URLSearchParams instances are merged', t => { 138 | const instanceA = got.extend({ 139 | searchParams: new URLSearchParams({a: '1'}) 140 | }); 141 | 142 | const instanceB = got.extend({ 143 | searchParams: new URLSearchParams({b: '2'}) 144 | }); 145 | 146 | const merged = got.mergeInstances(instanceA, instanceB); 147 | t.is(merged.defaults.options.searchParams.get('a'), '1'); 148 | t.is(merged.defaults.options.searchParams.get('b'), '2'); 149 | }); 150 | -------------------------------------------------------------------------------- /test/progress.ts: -------------------------------------------------------------------------------- 1 | import {promisify} from 'util'; 2 | import fs from 'fs'; 3 | import SlowStream from 'slow-stream'; 4 | import toReadableStream from 'to-readable-stream'; 5 | import getStream from 'get-stream'; 6 | import FormData from 'form-data'; 7 | import tempfile from 'tempfile'; 8 | import is from '@sindresorhus/is'; 9 | import test from 'ava'; 10 | import got from '../source'; 11 | import {createServer} from './helpers/server'; 12 | 13 | const checkEvents = (t, events, bodySize = undefined) => { 14 | t.true(events.length >= 2); 15 | 16 | const hasBodySize = is.number(bodySize); 17 | let lastEvent = events.shift(); 18 | 19 | if (!hasBodySize) { 20 | t.is(lastEvent.percent, 0); 21 | } 22 | 23 | for (const [index, event] of events.entries()) { 24 | if (hasBodySize) { 25 | t.is(event.percent, event.transferred / bodySize); 26 | t.true(event.percent > lastEvent.percent); 27 | } else { 28 | const isLastEvent = index === events.length - 1; 29 | t.is(event.percent, isLastEvent ? 1 : 0); 30 | } 31 | 32 | t.true(event.transferred >= lastEvent.transferred); 33 | t.is(event.total, bodySize); 34 | 35 | lastEvent = event; 36 | } 37 | }; 38 | 39 | const file = Buffer.alloc(1024 * 1024 * 2); 40 | let s; 41 | 42 | test.before('setup', async () => { 43 | s = await createServer(); 44 | 45 | s.on('/download', (request, response) => { 46 | response.setHeader('content-length', file.length); 47 | 48 | toReadableStream(file) 49 | .pipe(new SlowStream({maxWriteInterval: 50})) 50 | .pipe(response); 51 | }); 52 | 53 | s.on('/download/no-total', (request, response) => { 54 | response.write('hello'); 55 | response.end(); 56 | }); 57 | 58 | s.on('/upload', (request, response) => { 59 | request 60 | .pipe(new SlowStream({maxWriteInterval: 100})) 61 | .on('end', () => response.end()); 62 | }); 63 | 64 | await s.listen(s.port); 65 | }); 66 | 67 | test.after('cleanup', async () => { 68 | await s.close(); 69 | }); 70 | 71 | test('download progress', async t => { 72 | const events = []; 73 | 74 | const {body} = await got(`${s.url}/download`, {encoding: null}) 75 | .on('downloadProgress', event => events.push(event)); 76 | 77 | checkEvents(t, events, body.length); 78 | }); 79 | 80 | test('download progress - missing total size', async t => { 81 | const events = []; 82 | 83 | await got(`${s.url}/download/no-total`) 84 | .on('downloadProgress', event => events.push(event)); 85 | 86 | checkEvents(t, events); 87 | }); 88 | 89 | test('download progress - stream', async t => { 90 | const events = []; 91 | 92 | const stream = got.stream(`${s.url}/download`, {encoding: null}) 93 | .on('downloadProgress', event => events.push(event)); 94 | 95 | await getStream(stream); 96 | 97 | checkEvents(t, events, file.length); 98 | }); 99 | 100 | test('upload progress - file', async t => { 101 | const events = []; 102 | 103 | await got.post(`${s.url}/upload`, {body: file}) 104 | .on('uploadProgress', event => events.push(event)); 105 | 106 | checkEvents(t, events, file.length); 107 | }); 108 | 109 | test('upload progress - file stream', async t => { 110 | const path = tempfile(); 111 | fs.writeFileSync(path, file); 112 | 113 | const events = []; 114 | 115 | await got.post(`${s.url}/upload`, {body: fs.createReadStream(path)}) 116 | .on('uploadProgress', event => events.push(event)); 117 | 118 | checkEvents(t, events, file.length); 119 | }); 120 | 121 | test('upload progress - form data', async t => { 122 | const events = []; 123 | 124 | const body = new FormData(); 125 | body.append('key', 'value'); 126 | body.append('file', file); 127 | 128 | const size = await promisify(body.getLength.bind(body))(); 129 | 130 | await got.post(`${s.url}/upload`, {body}) 131 | .on('uploadProgress', event => events.push(event)); 132 | 133 | checkEvents(t, events, size); 134 | }); 135 | 136 | test('upload progress - json', async t => { 137 | const body = JSON.stringify({key: 'value'}); 138 | const size = Buffer.byteLength(body); 139 | const events = []; 140 | 141 | await got.post(`${s.url}/upload`, {body}) 142 | .on('uploadProgress', event => events.push(event)); 143 | 144 | checkEvents(t, events, size); 145 | }); 146 | 147 | test('upload progress - stream with known body size', async t => { 148 | const events = []; 149 | const options = { 150 | headers: {'content-length': file.length} 151 | }; 152 | 153 | const request = got.stream.post(`${s.url}/upload`, options) 154 | .on('uploadProgress', event => events.push(event)); 155 | 156 | await getStream(toReadableStream(file).pipe(request)); 157 | 158 | checkEvents(t, events, file.length); 159 | }); 160 | 161 | test('upload progress - stream with unknown body size', async t => { 162 | const events = []; 163 | 164 | const request = got.stream.post(`${s.url}/upload`) 165 | .on('uploadProgress', event => events.push(event)); 166 | 167 | await getStream(toReadableStream(file).pipe(request)); 168 | 169 | checkEvents(t, events); 170 | }); 171 | 172 | test('upload progress - no body', async t => { 173 | const events = []; 174 | 175 | await got.post(`${s.url}/upload`) 176 | .on('uploadProgress', event => events.push(event)); 177 | 178 | t.deepEqual(events, [ 179 | { 180 | percent: 0, 181 | transferred: 0, 182 | total: 0 183 | }, 184 | { 185 | percent: 1, 186 | transferred: 0, 187 | total: 0 188 | } 189 | ]); 190 | }); 191 | -------------------------------------------------------------------------------- /source/utils/timed-out.ts: -------------------------------------------------------------------------------- 1 | import net from 'net'; 2 | import {ClientRequest} from 'http'; 3 | 4 | export class TimeoutError extends Error { 5 | event: string; 6 | 7 | code: string; 8 | 9 | constructor(threshold: number, event: string) { 10 | super(`Timeout awaiting '${event}' for ${threshold}ms`); 11 | 12 | this.name = 'TimeoutError'; 13 | this.code = 'ETIMEDOUT'; 14 | this.event = event; 15 | } 16 | } 17 | 18 | const reentry: symbol = Symbol('reentry'); 19 | const noop = (): void => {}; 20 | 21 | export interface Delays { 22 | lookup?: number; 23 | connect?: number; 24 | secureConnect?: number; 25 | socket?: number; 26 | response?: number; 27 | send?: number; 28 | request?: number; 29 | } 30 | 31 | export default (request: ClientRequest, delays: Delays, options: any) => { 32 | /* istanbul ignore next: this makes sure timed-out isn't called twice */ 33 | if (Reflect.has(request, reentry)) { 34 | return; 35 | } 36 | 37 | (request as any)[reentry] = true; 38 | 39 | let stopNewTimeouts = false; 40 | 41 | const addTimeout = (delay: number, callback: (...args: any) => void, ...args: any): (() => void) => { 42 | // An error had been thrown before. Going further would result in uncaught errors. 43 | // See https://github.com/sindresorhus/got/issues/631#issuecomment-435675051 44 | if (stopNewTimeouts) { 45 | return noop; 46 | } 47 | 48 | // Event loop order is timers, poll, immediates. 49 | // The timed event may emit during the current tick poll phase, so 50 | // defer calling the handler until the poll phase completes. 51 | let immediate: NodeJS.Immediate; 52 | const timeout: NodeJS.Timeout = setTimeout(() => { 53 | immediate = setImmediate(callback, delay, ...args); 54 | /* istanbul ignore next: added in node v9.7.0 */ 55 | if (immediate.unref) { 56 | immediate.unref(); 57 | } 58 | }, delay); 59 | 60 | /* istanbul ignore next: in order to support electron renderer */ 61 | if (timeout.unref) { 62 | timeout.unref(); 63 | } 64 | 65 | const cancel = (): void => { 66 | clearTimeout(timeout); 67 | clearImmediate(immediate); 68 | }; 69 | 70 | cancelers.push(cancel); 71 | 72 | return cancel; 73 | }; 74 | 75 | const {host, hostname} = options; 76 | const timeoutHandler = (delay: number, event: string): void => { 77 | request.emit('error', new TimeoutError(delay, event)); 78 | request.abort(); 79 | }; 80 | 81 | const cancelers: (() => void)[] = []; 82 | const cancelTimeouts = (): void => { 83 | stopNewTimeouts = true; 84 | cancelers.forEach(cancelTimeout => cancelTimeout()); 85 | }; 86 | 87 | request.on('error', (error: Error): void => { 88 | if (error.message !== 'socket hang up') { 89 | cancelTimeouts(); 90 | } 91 | }); 92 | request.once('response', response => { 93 | response.once('end', cancelTimeouts); 94 | }); 95 | 96 | if (delays.request !== undefined) { 97 | addTimeout(delays.request, timeoutHandler, 'request'); 98 | } 99 | 100 | if (delays.socket !== undefined) { 101 | const socketTimeoutHandler = (): void => { 102 | timeoutHandler(delays.socket!, 'socket'); 103 | }; 104 | 105 | request.setTimeout(delays.socket, socketTimeoutHandler); 106 | 107 | // `request.setTimeout(0)` causes a memory leak. 108 | // We can just remove the listener and forget about the timer - it's unreffed. 109 | // See https://github.com/sindresorhus/got/issues/690 110 | cancelers.push((): void => { 111 | request.removeListener('timeout', socketTimeoutHandler); 112 | }); 113 | } 114 | 115 | request.once('socket', (socket: net.Socket): void => { 116 | const {socketPath} = request as any; 117 | 118 | /* istanbul ignore next: hard to test */ 119 | if (socket.connecting) { 120 | if (delays.lookup !== undefined && !socketPath && !net.isIP(hostname || host)) { 121 | const cancelTimeout = addTimeout(delays.lookup, timeoutHandler, 'lookup'); 122 | socket.once('lookup', cancelTimeout); 123 | } 124 | 125 | if (delays.connect !== undefined) { 126 | const timeConnect = () => addTimeout(delays.connect!, timeoutHandler, 'connect'); 127 | 128 | if (socketPath || net.isIP(hostname || host)) { 129 | socket.once('connect', timeConnect()); 130 | } else { 131 | socket.once('lookup', (error: Error): void => { 132 | if (error === null) { 133 | socket.once('connect', timeConnect()); 134 | } 135 | }); 136 | } 137 | } 138 | 139 | if (delays.secureConnect !== undefined && options.protocol === 'https:') { 140 | socket.once('connect', (): void => { 141 | const cancelTimeout = addTimeout(delays.secureConnect!, timeoutHandler, 'secureConnect'); 142 | socket.once('secureConnect', cancelTimeout); 143 | }); 144 | } 145 | } 146 | 147 | if (delays.send !== undefined) { 148 | const timeRequest = () => addTimeout(delays.send!, timeoutHandler, 'send'); 149 | /* istanbul ignore next: hard to test */ 150 | if (socket.connecting) { 151 | socket.once('connect', (): void => { 152 | request.once('upload-complete', timeRequest()); 153 | }); 154 | } else { 155 | request.once('upload-complete', timeRequest()); 156 | } 157 | } 158 | }); 159 | 160 | if (delays.response !== undefined) { 161 | request.once('upload-complete', (): void => { 162 | const cancelTimeout = addTimeout(delays.response!, timeoutHandler, 'response'); 163 | request.once('response', cancelTimeout); 164 | }); 165 | } 166 | 167 | return cancelTimeouts; 168 | }; 169 | -------------------------------------------------------------------------------- /test/error.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import test from 'ava'; 3 | import getStream from 'get-stream'; 4 | import proxyquire from 'proxyquire'; 5 | import got from '../source'; 6 | import {createServer} from './helpers/server'; 7 | 8 | let s; 9 | 10 | test.before('setup', async () => { 11 | s = await createServer(); 12 | 13 | s.on('/', (request, response) => { 14 | response.statusCode = 404; 15 | response.end('not'); 16 | }); 17 | 18 | s.on('/default-status-message', (request, response) => { 19 | response.statusCode = 400; 20 | response.end('body'); 21 | }); 22 | 23 | s.on('/custom-status-message', (request, response) => { 24 | response.statusCode = 400; 25 | response.statusMessage = 'Something Exploded'; 26 | response.end('body'); 27 | }); 28 | 29 | s.on('/no-status-message', (request, response) => { 30 | response.writeHead(400, ''); 31 | response.end('body'); 32 | }); 33 | 34 | s.on('/body', async (request, response) => { 35 | const body = await getStream(request); 36 | response.end(body); 37 | }); 38 | 39 | await s.listen(s.port); 40 | }); 41 | 42 | test.after('cleanup', async () => { 43 | await s.close(); 44 | }); 45 | 46 | test('properties', async t => { 47 | const error = await t.throwsAsync(got(s.url)) as any; 48 | t.truthy(error); 49 | // @ts-ignore 50 | t.truthy(error.response); 51 | t.false({}.propertyIsEnumerable.call(error, 'response')); 52 | t.false({}.hasOwnProperty.call(error, 'code')); 53 | t.is(error.message, 'Response code 404 (Not Found)'); 54 | t.is(error.host, `${s.host}:${s.port}`); 55 | t.is(error.method, 'GET'); 56 | t.is(error.protocol, 'http:'); 57 | t.is(error.url, error.response.requestUrl); 58 | t.is(error.headers.connection, 'close'); 59 | t.is(error.response.body, 'not'); 60 | }); 61 | 62 | test('dns message', async t => { 63 | const error = await t.throwsAsync(got('http://doesntexist', {retry: 0})) as any; 64 | t.truthy(error); 65 | t.regex(error.message, /getaddrinfo ENOTFOUND/); 66 | t.is(error.host, 'doesntexist'); 67 | t.is(error.method, 'GET'); 68 | }); 69 | 70 | test('options.body form error message', async t => { 71 | await t.throwsAsync(got.post(s.url, {body: Buffer.from('test'), form: ''}), { 72 | message: 'The `body` option cannot be used with the `json` option or `form` option' 73 | }); 74 | }); 75 | 76 | test('no plain object restriction on json body', async t => { 77 | function CustomObject() { 78 | this.a = 123; 79 | } 80 | 81 | const body = await got.post(`${s.url}/body`, {json: new CustomObject()}).json(); 82 | 83 | t.deepEqual(body, {a: 123}); 84 | }); 85 | 86 | test('default status message', async t => { 87 | const error = await t.throwsAsync(got(`${s.url}/default-status-message`)); 88 | // @ts-ignore 89 | t.is(error.statusCode, 400); 90 | // @ts-ignore 91 | t.is(error.statusMessage, 'Bad Request'); 92 | }); 93 | 94 | test('custom status message', async t => { 95 | const error = await t.throwsAsync(got(`${s.url}/custom-status-message`)); 96 | // @ts-ignore 97 | t.is(error.statusCode, 400); 98 | // @ts-ignore 99 | t.is(error.statusMessage, 'Something Exploded'); 100 | }); 101 | 102 | test('custom body', async t => { 103 | const error = await t.throwsAsync(got(s.url)); 104 | // @ts-ignore 105 | t.is(error.statusCode, 404); 106 | // @ts-ignore 107 | t.is(error.body, 'not'); 108 | }); 109 | 110 | test('contains Got options', async t => { 111 | const options = { 112 | url: s.url, 113 | auth: 'foo:bar' 114 | }; 115 | 116 | const error = await t.throwsAsync(got(options)); 117 | // @ts-ignore 118 | t.is(error.gotOptions.auth, options.auth); 119 | }); 120 | 121 | test('no status message is overriden by the default one', async t => { 122 | const error = await t.throwsAsync(got(`${s.url}/no-status-message`)); 123 | // @ts-ignore 124 | t.is(error.statusCode, 400); 125 | // @ts-ignore 126 | t.is(error.statusMessage, http.STATUS_CODES[400]); 127 | }); 128 | 129 | test('http.request error', async t => { 130 | await t.throwsAsync(got(s.url, { 131 | request: () => { 132 | throw new TypeError('The header content contains invalid characters'); 133 | } 134 | }), { 135 | instanceOf: got.RequestError, 136 | message: 'The header content contains invalid characters' 137 | }); 138 | }); 139 | 140 | test('http.request pipe error', async t => { 141 | const message = 'snap!'; 142 | 143 | await t.throwsAsync(got(s.url, { 144 | request: (...options) => { 145 | // @ts-ignore 146 | const modified = http.request(...options); 147 | modified.end = () => { 148 | modified.abort(); 149 | throw new Error(message); 150 | }; 151 | 152 | return modified; 153 | }, 154 | throwHttpErrors: false 155 | }), { 156 | instanceOf: got.RequestError, 157 | message 158 | }); 159 | }); 160 | 161 | test('http.request error through CacheableRequest', async t => { 162 | await t.throwsAsync(got(s.url, { 163 | request: () => { 164 | throw new TypeError('The header content contains invalid characters'); 165 | }, 166 | cache: new Map() 167 | }), { 168 | instanceOf: got.RequestError, 169 | message: 'The header content contains invalid characters' 170 | }); 171 | }); 172 | 173 | test('catch error in mimicResponse', async t => { 174 | const mimicResponse = () => { 175 | throw new Error('Error in mimic-response'); 176 | }; 177 | 178 | mimicResponse['@global'] = true; 179 | 180 | const proxiedGot = proxyquire('../source', { 181 | 'mimic-response': mimicResponse 182 | }); 183 | 184 | // @ts-ignore 185 | await t.throwsAsync(proxiedGot(s.url), {message: 'Error in mimic-response'}); 186 | }); 187 | 188 | test('errors are thrown directly when options.stream is true', t => { 189 | t.throws(() => got(s.url, {stream: true, hooks: false}), { 190 | message: 'Parameter `hooks` must be an object, not boolean' 191 | }); 192 | }); 193 | -------------------------------------------------------------------------------- /source/known-hook-events.ts: -------------------------------------------------------------------------------- 1 | import {IncomingMessage} from 'http'; 2 | import {Options, CancelableRequest} from './utils/types'; 3 | import {HTTPError} from './errors'; 4 | 5 | /** 6 | * Called with plain request options, right before their normalization. This is especially useful in conjunction with got.extend() and got.create() when the input needs custom handling. 7 | * 8 | * **Note:** This hook must be synchronous. 9 | * 10 | * @see [Request migration guide](https://github.com/sindresorhus/got/blob/master/migration-guides.md#breaking-changes) for an example. 11 | */ 12 | export type InitHook = (options: Options) => void; 13 | 14 | /** 15 | * Called with normalized [request options](https://github.com/sindresorhus/got#options). Got will make no further changes to the request before it is sent (except the body serialization). This is especially useful in conjunction with [`got.extend()`](https://github.com/sindresorhus/got#instances) and [`got.create()`](https://github.com/sindresorhus/got/blob/master/advanced-creation.md) when you want to create an API client that, for example, uses HMAC-signing. 16 | * 17 | * @see [AWS section](https://github.com/sindresorhus/got#aws) for an example. 18 | */ 19 | export type BeforeRequestHook = (options: Options) => void | Promise; 20 | 21 | /** 22 | * Called with normalized [request options](https://github.com/sindresorhus/got#options). Got will make no further changes to the request. This is especially useful when you want to avoid dead sites. 23 | */ 24 | export type BeforeRedirectHook = (options: Options) => void | Promise; 25 | 26 | /** 27 | * Called with normalized [request options](https://github.com/sindresorhus/got#options), the error and the retry count. Got will make no further changes to the request. This is especially useful when some extra work is required before the next try. 28 | */ 29 | export type BeforeRetryHook = (options: Options, error: HTTPError, retryCount: number) => void | Promise; 30 | 31 | // TODO: The `Error` type should conform to any possible extended error type that can be thrown. See https://github.com/sindresorhus/got#hooksbeforeerror 32 | /** 33 | * Called with an `Error` instance. The error is passed to the hook right before it's thrown. This is especially useful when you want to have more detailed errors. 34 | * 35 | * **Note:** Errors thrown while normalizing input options are thrown directly and not part of this hook. 36 | */ 37 | export type BeforeErrorHook = (error: Error) => Error | Promise; 38 | 39 | /** 40 | * Called with [response object](https://github.com/sindresorhus/got#response) and a retry function. 41 | * 42 | * Each function should return the response. This is especially useful when you want to refresh an access token. 43 | */ 44 | export type AfterResponseHook = (response: IncomingMessage, retryWithMergedOptions: (options: Options) => CancelableRequest) => IncomingMessage | CancelableRequest; 45 | 46 | export type HookType = BeforeErrorHook | InitHook | BeforeRequestHook | BeforeRedirectHook | BeforeRetryHook | AfterResponseHook; 47 | 48 | /** 49 | * Hooks allow modifications during the request lifecycle. Hook functions may be async and are run serially. 50 | */ 51 | export interface Hooks { 52 | /** 53 | * Called with plain request options, right before their normalization. This is especially useful in conjunction with got.extend() and got.create() when the input needs custom handling. 54 | * 55 | * **Note:** This hook must be synchronous. 56 | * 57 | * @see [Request migration guide](https://github.com/sindresorhus/got/blob/master/migration-guides.md#breaking-changes) for an example. 58 | * @default [] 59 | */ 60 | init: InitHook[]; 61 | 62 | /** 63 | * Called with normalized [request options](https://github.com/sindresorhus/got#options). Got will make no further changes to the request before it is sent (except the body serialization). This is especially useful in conjunction with [`got.extend()`](https://github.com/sindresorhus/got#instances) and [`got.create()`](https://github.com/sindresorhus/got/blob/master/advanced-creation.md) when you want to create an API client that, for example, uses HMAC-signing. 64 | * 65 | * @see [AWS section](https://github.com/sindresorhus/got#aws) for an example. 66 | * @default [] 67 | */ 68 | beforeRequest: BeforeRequestHook[]; 69 | 70 | /** 71 | * Called with normalized [request options](https://github.com/sindresorhus/got#options). Got will make no further changes to the request. This is especially useful when you want to avoid dead sites. 72 | * 73 | * @default [] 74 | */ 75 | beforeRedirect: BeforeRedirectHook[]; 76 | 77 | /** 78 | * Called with normalized [request options](https://github.com/sindresorhus/got#options), the error and the retry count. Got will make no further changes to the request. This is especially useful when some extra work is required before the next try. 79 | * 80 | * @default [] 81 | */ 82 | beforeRetry: BeforeRetryHook[]; 83 | 84 | /** 85 | * Called with an `Error` instance. The error is passed to the hook right before it's thrown. This is especially useful when you want to have more detailed errors. 86 | * 87 | * **Note:** Errors thrown while normalizing input options are thrown directly and not part of this hook. 88 | * 89 | * @default [] 90 | */ 91 | beforeError: BeforeErrorHook[]; 92 | 93 | /** 94 | * Called with [response object](https://github.com/sindresorhus/got#response) and a retry function. 95 | * 96 | * Each function should return the response. This is especially useful when you want to refresh an access token. 97 | * 98 | * @default [] 99 | */ 100 | afterResponse: AfterResponseHook[]; 101 | } 102 | 103 | export type HookEvent = keyof Hooks; 104 | 105 | const knownHookEvents: HookEvent[] = [ 106 | 'beforeError', 107 | 'init', 108 | 'beforeRequest', 109 | 'beforeRedirect', 110 | 'beforeRetry', 111 | 'afterResponse' 112 | ]; 113 | 114 | export default knownHookEvents; 115 | -------------------------------------------------------------------------------- /migration-guides.md: -------------------------------------------------------------------------------- 1 | # Migration guides 2 | 3 | > :star: Switching from other HTTP request libraries to Got :star: 4 | 5 | ### Migrating from Request 6 | 7 | You may think it's too hard to switch, but it's really not. 🦄 8 | 9 | Let's take the very first example from Request's readme: 10 | 11 | ```js 12 | const request = require('request'); 13 | 14 | request('https://google.com', (error, response, body) => { 15 | console.log('error:', error); // Print the error if one occurred 16 | console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received 17 | console.log('body:', body); // Print the HTML for the Google homepage 18 | }); 19 | ``` 20 | 21 | With Got, it is: 22 | 23 | ```js 24 | const got = require('got'); 25 | 26 | (async () => { 27 | try { 28 | const response = await got('https://google.com'); 29 | console.log('statusCode:', response.statusCode); 30 | console.log('body:', response.body); 31 | } catch (error) { 32 | console.log('error:', error); 33 | } 34 | })(); 35 | ``` 36 | 37 | Looks better now, huh? 😎 38 | 39 | #### Common options 40 | 41 | Both Request and Got accept [`http.request` options](https://nodejs.org/api/http.html#http_http_request_options_callback). 42 | 43 | These Got options are the same as with Request: 44 | 45 | - [`url`](https://github.com/sindresorhus/got#url) (+ we accept [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) instances too!) 46 | - [`body`](https://github.com/sindresorhus/got#body) 47 | - [`followRedirect`](https://github.com/sindresorhus/got#followRedirect) 48 | - [`encoding`](https://github.com/sindresorhus/got#encoding) 49 | 50 | So if you're familiar with them, you're good to go. 51 | 52 | Oh, and one more thing... There's no `time` option. Assume [it's always true](https://github.com/sindresorhus/got#timings). 53 | 54 | #### Renamed options 55 | 56 | Readability is very important to us, so we have different names for these options: 57 | 58 | - `qs` → [`searchParams`](https://github.com/sindresorhus/got#searchParams) 59 | - `strictSSL` → [`rejectUnauthorized`](https://github.com/sindresorhus/got#rejectUnauthorized) 60 | - `gzip` → [`decompress`](https://github.com/sindresorhus/got#decompress) 61 | - `jar` → [`cookieJar`](https://github.com/sindresorhus/got#cookiejar) (accepts [`tough-cookie`](https://github.com/salesforce/tough-cookie) jar) 62 | 63 | It's more clear, isn't it? 64 | 65 | #### Changes in behavior 66 | 67 | The [`timeout` option](https://github.com/sindresorhus/got#timeout) has some extra features. You can [set timeouts on particular events](readme.md#timeout)! 68 | 69 | The [`searchParams` option](https://github.com/sindresorhus/got#searchParams) is always serialized using [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) unless it's a `string`. 70 | 71 | The [`baseUrl` option](https://github.com/sindresorhus/got#baseurl) appends the ending slash if it's not present. 72 | 73 | There's no `maxRedirects` option. It's always set to `10`. 74 | 75 | To use streams, just call `got.stream(url, options)` or `got(url, {stream: true, ...}`). 76 | 77 | #### Breaking changes 78 | 79 | - The `json` option is not a `boolean`, it's an `Object`. It will be stringified and used as a body. 80 | - No `form` option. You have to pass a [`form-data` instance](https://github.com/form-data/form-data) through the [`body` option](https://github.com/sindresorhus/got#body). 81 | - No `oauth`/`hawk`/`aws`/`httpSignature` option. To sign requests, you need to create a [custom instance](advanced-creation.md#signing-requests). 82 | - No `agentClass`/`agentOptions`/`pool` option. 83 | - No `forever` option. You need to use [forever-agent](https://github.com/request/forever-agent). 84 | - No `proxy` option. You need to [pass a custom agent](readme.md#proxies). 85 | - No `removeRefererHeader` option. You can remove the referer header in a [`beforeRequest` hook](https://github.com/sindresorhus/got#hooksbeforeRequest): 86 | 87 | ```js 88 | const gotInstance = got.extend({ 89 | hooks: { 90 | beforeRequest: [ 91 | options => { 92 | delete options.headers.referer; 93 | } 94 | ] 95 | } 96 | }); 97 | 98 | gotInstance(url, options); 99 | ``` 100 | 101 | - No `jsonReviver`/`jsonReplacer` option, but you can use hooks for that too: 102 | 103 | ```js 104 | const gotInstance = got.extend({ 105 | hooks: { 106 | init: [ 107 | options => { 108 | if (options.jsonReplacer && options.body) { 109 | options.body = JSON.stringify(options.body, options.jsonReplacer); 110 | } 111 | } 112 | ], 113 | afterResponse: [ 114 | response => { 115 | const options = response.request.gotOptions; 116 | if (options.jsonReviver && options.responseType === 'json') { 117 | options.responseType = ''; 118 | response.body = JSON.parse(response.body, options.jsonReviver); 119 | } 120 | 121 | return response; 122 | } 123 | ] 124 | } 125 | }); 126 | 127 | gotInstance(url, options); 128 | ``` 129 | 130 | Hooks are powerful, aren't they? [Read more](readme.md#hooks) to see what else you achieve using hooks. 131 | 132 | #### More about streams 133 | 134 | Let's take a quick look at another example from Request's readme: 135 | 136 | ```js 137 | http.createServer((req, res) => { 138 | if (req.url === '/doodle.png') { 139 | req.pipe(request('https://example.com/doodle.png')).pipe(res); 140 | } 141 | }); 142 | ``` 143 | 144 | The cool feature here is that Request can proxy headers with the stream, but Got can do that too: 145 | 146 | ```js 147 | http.createServer((req, res) => { 148 | if (req.url === '/doodle.png') { 149 | req.pipe(got.stream('https://example.com/doodle.png')).pipe(res); 150 | } 151 | }); 152 | ``` 153 | 154 | Nothing has really changed. Just remember to use `got.stream(url, options)` or `got(url, {stream: true, …`}). That's it! 155 | 156 | #### You're good to go! 157 | 158 | Well, you have already come this far. Take a look at the [documentation](readme.md#highlights). It's worth the time to read it. There are [some great tips](readme.md#aborting-the-request). If something is unclear or doesn't work as it should, don't hesitate to open an issue. 159 | -------------------------------------------------------------------------------- /test/stream.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import toReadableStream from 'to-readable-stream'; 3 | import getStream from 'get-stream'; 4 | import pEvent from 'p-event'; 5 | import delay from 'delay'; 6 | import is from '@sindresorhus/is'; 7 | import got from '../source'; 8 | import {createServer} from './helpers/server'; 9 | 10 | let s; 11 | 12 | test.before('setup', async () => { 13 | s = await createServer(); 14 | 15 | s.on('/', (request, response) => { 16 | response.writeHead(200, { 17 | unicorn: 'rainbow', 18 | 'content-encoding': 'gzip' 19 | }); 20 | response.end(Buffer.from('H4sIAAAAAAAA/8vPBgBH3dx5AgAAAA==', 'base64')); // 'ok' 21 | }); 22 | 23 | s.on('/post', (request, response) => { 24 | request.pipe(response); 25 | }); 26 | 27 | s.on('/redirect', (request, response) => { 28 | response.writeHead(302, { 29 | location: s.url 30 | }); 31 | response.end(); 32 | }); 33 | 34 | s.on('/error', (request, response) => { 35 | response.statusCode = 404; 36 | response.end(); 37 | }); 38 | 39 | await s.listen(s.port); 40 | }); 41 | 42 | test.after('cleanup', async () => { 43 | await s.close(); 44 | }); 45 | 46 | test('options.responseType is ignored', t => { 47 | t.notThrows(() => got.stream(s.url, {responseType: 'json'})); 48 | }); 49 | 50 | test('returns readable stream', async t => { 51 | const data = await pEvent(got.stream(s.url), 'data'); 52 | t.is(data.toString(), 'ok'); 53 | }); 54 | 55 | test('returns writeable stream', async t => { 56 | const stream = got.stream.post(`${s.url}/post`); 57 | const promise = pEvent(stream, 'data'); 58 | stream.end('wow'); 59 | t.is((await promise).toString(), 'wow'); 60 | }); 61 | 62 | test('throws on write to stream with body specified', t => { 63 | t.throws(() => { 64 | got.stream.post(s.url, {body: 'wow'}).end('wow'); 65 | }, 'Got\'s stream is not writable when the `body` option is used'); 66 | }); 67 | 68 | test('have request event', async t => { 69 | const request = await pEvent(got.stream(s.url), 'request'); 70 | t.truthy(request); 71 | // @ts-ignore 72 | t.is(request.method, 'GET'); 73 | }); 74 | 75 | test('have redirect event', async t => { 76 | const {headers} = await pEvent(got.stream(`${s.url}/redirect`), 'redirect'); 77 | t.is(headers.location, s.url); 78 | }); 79 | 80 | test('have response event', async t => { 81 | const {statusCode} = await pEvent(got.stream(s.url), 'response'); 82 | t.is(statusCode, 200); 83 | }); 84 | 85 | test('have error event', async t => { 86 | const stream = got.stream(`${s.url}/error`, {retry: 0}); 87 | await t.throwsAsync(pEvent(stream, 'response'), /Response code 404 \(Not Found\)/); 88 | }); 89 | 90 | test('have error event #2', async t => { 91 | const stream = got.stream('http://doesntexist', {retry: 0}); 92 | await t.throwsAsync(pEvent(stream, 'response'), /getaddrinfo ENOTFOUND/); 93 | }); 94 | 95 | test('have response event on throwHttpErrors === false', async t => { 96 | const {statusCode} = await pEvent(got.stream(`${s.url}/error`, {throwHttpErrors: false}), 'response'); 97 | t.is(statusCode, 404); 98 | }); 99 | 100 | test('accepts option.body as Stream', async t => { 101 | const stream = got.stream.post(`${s.url}/post`, {body: toReadableStream('wow')}); 102 | const data = await pEvent(stream, 'data'); 103 | t.is(data.toString(), 'wow'); 104 | }); 105 | 106 | test('redirect response contains old url', async t => { 107 | const {requestUrl} = await pEvent(got.stream(`${s.url}/redirect`), 'response'); 108 | t.is(requestUrl, `${s.url}/redirect`); 109 | }); 110 | 111 | test('check for pipe method', t => { 112 | const stream = got.stream(`${s.url}/`); 113 | t.true(is.function_(stream.pipe)); 114 | t.true(is.function_(stream.on('error', () => {}).pipe)); 115 | }); 116 | 117 | test('piping works', async t => { 118 | t.is(await getStream(got.stream(`${s.url}/`)), 'ok'); 119 | t.is(await getStream(got.stream(`${s.url}/`).on('error', () => {})), 'ok'); 120 | }); 121 | 122 | test('proxying headers works', async t => { 123 | const server = await createServer(); 124 | 125 | server.on('/', (request, response) => { 126 | got.stream(s.url).pipe(response); 127 | }); 128 | 129 | await server.listen(server.port); 130 | 131 | const {headers, body} = await got(server.url); 132 | t.is(headers.unicorn, 'rainbow'); 133 | t.is(headers['content-encoding'], undefined); 134 | t.is(body, 'ok'); 135 | 136 | await server.close(); 137 | }); 138 | 139 | test('skips proxying headers after server has sent them already', async t => { 140 | const server = await createServer(); 141 | 142 | server.on('/', (request, response) => { 143 | response.writeHead(200); 144 | got.stream(s.url).pipe(response); 145 | }); 146 | 147 | await server.listen(server.port); 148 | 149 | const {headers} = await got(server.url); 150 | t.is(headers.unicorn, undefined); 151 | 152 | await server.close(); 153 | }); 154 | 155 | test('throws when trying to proxy through a closed stream', async t => { 156 | const server = await createServer(); 157 | 158 | server.on('/', async (request, response) => { 159 | const stream = got.stream(s.url); 160 | await delay(1000); 161 | t.throws(() => stream.pipe(response)); 162 | response.end(); 163 | }); 164 | 165 | await server.listen(server.port); 166 | await got(server.url); 167 | await server.close(); 168 | }); 169 | 170 | test('proxies content-encoding header when options.decompress is false', async t => { 171 | const server = await createServer(); 172 | 173 | server.on('/', (request, response) => { 174 | got.stream(s.url, {decompress: false}).pipe(response); 175 | }); 176 | 177 | await server.listen(server.port); 178 | 179 | const {headers} = await got(server.url); 180 | t.is(headers.unicorn, 'rainbow'); 181 | t.is(headers['content-encoding'], 'gzip'); 182 | 183 | await server.close(); 184 | }); 185 | 186 | test('destroying got.stream() cancels the request', async t => { 187 | const stream = got.stream(s.url); 188 | const request = await pEvent(stream, 'request'); 189 | stream.destroy(); 190 | // @ts-ignore 191 | t.truthy(request.aborted); 192 | }); 193 | 194 | test('piping to got.stream.put()', async t => { 195 | await t.notThrowsAsync(async () => { 196 | await getStream(got.stream(s.url).pipe(got.stream.put(`${s.url}/post`))); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /test/create.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import {URL} from 'url'; 3 | import test from 'ava'; 4 | import got from '../source'; 5 | import {createServer} from './helpers/server'; 6 | 7 | let s; 8 | 9 | test.before('setup', async () => { 10 | s = await createServer(); 11 | 12 | s.on('/', (request, response) => { 13 | request.resume(); 14 | response.end(JSON.stringify(request.headers)); 15 | }); 16 | 17 | await s.listen(s.port); 18 | }); 19 | 20 | test.after('cleanup', async () => { 21 | await s.close(); 22 | }); 23 | 24 | test('preserve global defaults', async t => { 25 | const globalHeaders = await got(s.url).json(); 26 | const instanceHeaders = await got.extend()(s.url).json(); 27 | t.deepEqual(instanceHeaders, globalHeaders); 28 | }); 29 | 30 | test('support instance defaults', async t => { 31 | const instance = got.extend({ 32 | headers: { 33 | 'user-agent': 'custom-ua-string' 34 | } 35 | }); 36 | const headers = await instance(s.url).json(); 37 | t.is(headers['user-agent'], 'custom-ua-string'); 38 | }); 39 | 40 | test('support invocation overrides', async t => { 41 | const instance = got.extend({ 42 | headers: { 43 | 'user-agent': 'custom-ua-string' 44 | } 45 | }); 46 | const headers = await instance(s.url, { 47 | headers: { 48 | 'user-agent': 'different-ua-string' 49 | } 50 | }).json(); 51 | t.is(headers['user-agent'], 'different-ua-string'); 52 | }); 53 | 54 | test('curry previous instance defaults', async t => { 55 | const instanceA = got.extend({ 56 | headers: { 57 | 'x-foo': 'foo' 58 | } 59 | }); 60 | const instanceB = instanceA.extend({ 61 | headers: { 62 | 'x-bar': 'bar' 63 | } 64 | }); 65 | const headers = await instanceB(s.url).json(); 66 | t.is(headers['x-foo'], 'foo'); 67 | t.is(headers['x-bar'], 'bar'); 68 | }); 69 | 70 | test('custom headers (extend)', async t => { 71 | const options = {headers: {unicorn: 'rainbow'}}; 72 | 73 | const instance = got.extend(options); 74 | const headers = await instance(`${s.url}/`).json(); 75 | t.is(headers.unicorn, 'rainbow'); 76 | }); 77 | 78 | test('extend overwrites arrays with a deep clone', t => { 79 | const beforeRequest = [0]; 80 | const a = got.extend({hooks: {beforeRequest}}); 81 | beforeRequest[0] = 1; 82 | t.deepEqual(a.defaults.options.hooks.beforeRequest, [0]); 83 | t.not(a.defaults.options.hooks.beforeRequest, beforeRequest); 84 | }); 85 | 86 | test('extend keeps the old value if the new one is undefined', t => { 87 | const a = got.extend({headers: undefined}); 88 | t.deepEqual( 89 | a.defaults.options.headers, 90 | got.defaults.options.headers 91 | ); 92 | }); 93 | 94 | test('extend merges URL instances', t => { 95 | const a = got.extend({baseUrl: new URL('https://example.com')}); 96 | const b = a.extend({baseUrl: '/foo'}); 97 | t.is(b.defaults.options.baseUrl.toString(), 'https://example.com/foo/'); 98 | }); 99 | 100 | test('create', async t => { 101 | const instance = got.create({ 102 | options: {}, 103 | handler: (options, next) => { 104 | options.headers.unicorn = 'rainbow'; 105 | return next(options); 106 | } 107 | }); 108 | const headers = await instance(s.url).json(); 109 | t.is(headers.unicorn, 'rainbow'); 110 | t.is(headers['user-agent'], undefined); 111 | }); 112 | 113 | test('hooks are merged on got.extend()', t => { 114 | const hooksA = [() => {}]; 115 | const hooksB = [() => {}]; 116 | 117 | const instanceA = got.create({options: {hooks: {beforeRequest: hooksA}}}); 118 | 119 | const extended = instanceA.extend({hooks: {beforeRequest: hooksB}}); 120 | t.deepEqual(extended.defaults.options.hooks.beforeRequest, hooksA.concat(hooksB)); 121 | }); 122 | 123 | test('custom endpoint with custom headers (extend)', async t => { 124 | const instance = got.extend({headers: {unicorn: 'rainbow'}, baseUrl: s.url}); 125 | const headers = await instance('/').json(); 126 | t.is(headers.unicorn, 'rainbow'); 127 | t.not(headers['user-agent'], undefined); 128 | }); 129 | 130 | test('no tampering with defaults', t => { 131 | const instance = got.create({ 132 | handler: got.defaults.handler, 133 | options: got.mergeOptions(got.defaults.options, { 134 | baseUrl: 'example/' 135 | }) 136 | }); 137 | 138 | const instance2 = instance.create({ 139 | handler: instance.defaults.handler, 140 | options: instance.defaults.options 141 | }); 142 | 143 | // Tamper Time 144 | t.throws(() => { 145 | instance.defaults.options.baseUrl = 'http://google.com'; 146 | }); 147 | 148 | t.is(instance.defaults.options.baseUrl, 'example/'); 149 | t.is(instance2.defaults.options.baseUrl, 'example/'); 150 | }); 151 | 152 | test('defaults can be mutable', t => { 153 | const instance = got.create({ 154 | mutableDefaults: true, 155 | options: { 156 | followRedirect: false 157 | } 158 | }); 159 | 160 | t.notThrows(() => { 161 | instance.defaults.options.followRedirect = true; 162 | }); 163 | 164 | t.true(instance.defaults.options.followRedirect); 165 | }); 166 | 167 | test('can set mutable defaults using got.extend', t => { 168 | const instance = got.extend({ 169 | mutableDefaults: true, 170 | followRedirect: false 171 | }); 172 | 173 | t.notThrows(() => { 174 | instance.defaults.options.followRedirect = true; 175 | }); 176 | 177 | t.true(instance.defaults.options.followRedirect); 178 | }); 179 | 180 | test('only plain objects are freezed', async t => { 181 | const instance = got.extend({ 182 | agent: new http.Agent({keepAlive: true}) 183 | }); 184 | 185 | await t.notThrowsAsync(() => instance(s.url)); 186 | }); 187 | 188 | test('defaults are cloned on instance creation', t => { 189 | const options = {foo: 'bar', hooks: {beforeRequest: [() => {}]}}; 190 | const instance = got.create({options}); 191 | 192 | t.notThrows(() => { 193 | options.foo = 'foo'; 194 | delete options.hooks.beforeRequest[0]; 195 | }); 196 | 197 | t.not(options.foo, instance.defaults.options.foo); 198 | t.not(options.hooks.beforeRequest, instance.defaults.options.hooks.beforeRequest); 199 | }); 200 | 201 | test('ability to pass a custom request method', async t => { 202 | let called = false; 203 | 204 | const request = (...args) => { 205 | called = true; 206 | // @ts-ignore 207 | return http.request(...args); 208 | }; 209 | 210 | const instance = got.extend({request}); 211 | await instance(s.url); 212 | 213 | t.true(called); 214 | }); 215 | 216 | test('hooks aren\'t overriden when merging options', async t => { 217 | let called = false; 218 | const instance = got.extend({ 219 | hooks: { 220 | beforeRequest: [ 221 | () => { 222 | called = true; 223 | } 224 | ] 225 | } 226 | }); 227 | 228 | await instance(s.url, {}); 229 | 230 | t.true(called); 231 | }); 232 | -------------------------------------------------------------------------------- /test/headers.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import {promisify} from 'util'; 3 | import path from 'path'; 4 | import test from 'ava'; 5 | import FormData from 'form-data'; 6 | import got from '../source'; 7 | import supportsBrotli from '../source/utils/supports-brotli'; 8 | import pkg from '../package.json'; 9 | import {createServer} from './helpers/server'; 10 | 11 | let s; 12 | 13 | test.before('setup', async () => { 14 | s = await createServer(); 15 | 16 | s.on('/', (request, response) => { 17 | request.resume(); 18 | response.end(JSON.stringify(request.headers)); 19 | }); 20 | 21 | await s.listen(s.port); 22 | }); 23 | 24 | test.after('cleanup', async () => { 25 | await s.close(); 26 | }); 27 | 28 | test('user-agent', async t => { 29 | const headers = await got(s.url).json(); 30 | t.is(headers['user-agent'], `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`); 31 | }); 32 | 33 | test('accept-encoding', async t => { 34 | const headers = await got(s.url).json(); 35 | t.is(headers['accept-encoding'], supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate'); 36 | }); 37 | 38 | test('do not override accept-encoding', async t => { 39 | const headers = await got(s.url, { 40 | headers: { 41 | 'accept-encoding': 'gzip' 42 | } 43 | }).json(); 44 | t.is(headers['accept-encoding'], 'gzip'); 45 | }); 46 | 47 | test('do not remove user headers from `url` object argument', async t => { 48 | const headers = (await got({ 49 | hostname: s.host, 50 | port: s.port, 51 | responseType: 'json', 52 | protocol: 'http:', 53 | headers: { 54 | 'X-Request-Id': 'value' 55 | } 56 | })).body; 57 | 58 | t.is(headers.accept, 'application/json'); 59 | t.is(headers['user-agent'], `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`); 60 | t.is(headers['accept-encoding'], supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate'); 61 | t.is(headers['x-request-id'], 'value'); 62 | }); 63 | 64 | test('do not set accept-encoding header when decompress options is false', async t => { 65 | const headers = await got(s.url, { 66 | decompress: false 67 | }).json(); 68 | t.false(Reflect.has(headers, 'accept-encoding')); 69 | }); 70 | 71 | test('accept header with json option', async t => { 72 | let headers = await got(s.url).json(); 73 | t.is(headers.accept, 'application/json'); 74 | 75 | headers = await got(s.url, { 76 | headers: { 77 | accept: '' 78 | } 79 | }).json(); 80 | t.is(headers.accept, ''); 81 | }); 82 | 83 | test('host', async t => { 84 | const headers = await got(s.url).json(); 85 | t.is(headers.host, `localhost:${s.port}`); 86 | }); 87 | 88 | test('transform names to lowercase', async t => { 89 | const headers = (await got(s.url, { 90 | headers: { 91 | 'ACCEPT-ENCODING': 'identity' 92 | }, 93 | responseType: 'json' 94 | })).body; 95 | t.is(headers['accept-encoding'], 'identity'); 96 | }); 97 | 98 | test('setting content-length to 0', async t => { 99 | const {body} = await got.post(s.url, { 100 | headers: { 101 | 'content-length': 0 102 | }, 103 | body: 'sup' 104 | }); 105 | const headers = JSON.parse(body); 106 | t.is(headers['content-length'], '0'); 107 | }); 108 | 109 | test('sets content-length to 0 when requesting PUT with empty body', async t => { 110 | const {body} = await got(s.url, { 111 | method: 'PUT' 112 | }); 113 | const headers = JSON.parse(body); 114 | t.is(headers['content-length'], '0'); 115 | }); 116 | 117 | test('form-data manual content-type', async t => { 118 | const form = new FormData(); 119 | form.append('a', 'b'); 120 | const {body} = await got.post(s.url, { 121 | headers: { 122 | 'content-type': 'custom' 123 | }, 124 | body: form 125 | }); 126 | const headers = JSON.parse(body); 127 | t.is(headers['content-type'], 'custom'); 128 | }); 129 | 130 | test('form-data automatic content-type', async t => { 131 | const form = new FormData(); 132 | form.append('a', 'b'); 133 | const {body} = await got.post(s.url, { 134 | body: form 135 | }); 136 | const headers = JSON.parse(body); 137 | t.is(headers['content-type'], `multipart/form-data; boundary=${form.getBoundary()}`); 138 | }); 139 | 140 | test('form-data sets content-length', async t => { 141 | const form = new FormData(); 142 | form.append('a', 'b'); 143 | const {body} = await got.post(s.url, {body: form}); 144 | const headers = JSON.parse(body); 145 | t.is(headers['content-length'], '157'); 146 | }); 147 | 148 | test('stream as options.body sets content-length', async t => { 149 | const fixture = path.join(__dirname, 'fixtures/stream-content-length'); 150 | const {size} = await promisify(fs.stat)(fixture); 151 | const {body} = await got.post(s.url, { 152 | body: fs.createReadStream(fixture) 153 | }); 154 | const headers = JSON.parse(body); 155 | t.is(Number(headers['content-length']), size); 156 | }); 157 | 158 | test('buffer as options.body sets content-length', async t => { 159 | const buffer = Buffer.from('unicorn'); 160 | const {body} = await got.post(s.url, { 161 | body: buffer 162 | }); 163 | const headers = JSON.parse(body); 164 | t.is(Number(headers['content-length']), buffer.length); 165 | }); 166 | 167 | test('remove null value headers', async t => { 168 | const {body} = await got(s.url, { 169 | headers: { 170 | 'user-agent': null 171 | } 172 | }); 173 | const headers = JSON.parse(body); 174 | t.false(Reflect.has(headers, 'user-agent')); 175 | }); 176 | 177 | test('setting a header to undefined keeps the old value', async t => { 178 | const {body} = await got(s.url, { 179 | headers: { 180 | 'user-agent': undefined 181 | } 182 | }); 183 | const headers = JSON.parse(body); 184 | t.not(headers['user-agent'], undefined); 185 | }); 186 | 187 | test('non-existent headers set to undefined are omitted', async t => { 188 | const {body} = await got(s.url, { 189 | headers: { 190 | blah: undefined 191 | } 192 | }); 193 | const headers = JSON.parse(body); 194 | t.false(Reflect.has(headers, 'blah')); 195 | }); 196 | 197 | test('preserve port in host header if non-standard port', async t => { 198 | const body = await got(s.url).json(); 199 | t.is(body.host, `localhost:${s.port}`); 200 | }); 201 | 202 | test('strip port in host header if explicit standard port (:80) & protocol (HTTP)', async t => { 203 | const body = await got('http://httpbin.org:80/headers').json(); 204 | t.is(body.headers.Host, 'httpbin.org'); 205 | }); 206 | 207 | test('strip port in host header if explicit standard port (:443) & protocol (HTTPS)', async t => { 208 | const body = await got('https://httpbin.org:443/headers').json(); 209 | t.is(body.headers.Host, 'httpbin.org'); 210 | }); 211 | 212 | test('strip port in host header if implicit standard port & protocol (HTTP)', async t => { 213 | const body = await got('http://httpbin.org/headers').json(); 214 | t.is(body.headers.Host, 'httpbin.org'); 215 | }); 216 | 217 | test('strip port in host header if implicit standard port & protocol (HTTPS)', async t => { 218 | const body = await got('https://httpbin.org/headers').json(); 219 | t.is(body.headers.Host, 'httpbin.org'); 220 | }); 221 | -------------------------------------------------------------------------------- /test/arguments.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-deprecated-api */ 2 | import {URL, URLSearchParams, parse} from 'url'; 3 | import test from 'ava'; 4 | import pEvent from 'p-event'; 5 | import got from '../source'; 6 | import {createServer} from './helpers/server'; 7 | 8 | let s; 9 | 10 | test.before('setup', async () => { 11 | s = await createServer(); 12 | 13 | const echoUrl = (request, response) => { 14 | response.end(request.url); 15 | }; 16 | 17 | s.on('/', (request, response) => { 18 | response.statusCode = 404; 19 | response.end(); 20 | }); 21 | 22 | s.on('/test', echoUrl); 23 | s.on('/?test=wow', echoUrl); 24 | s.on('/test/foobar', echoUrl); 25 | s.on('/?test=it’s+ok', echoUrl); 26 | s.on('/?test=http://example.com?foo=bar', echoUrl); 27 | 28 | s.on('/stream', (request, response) => { 29 | response.end('ok'); 30 | }); 31 | 32 | await s.listen(s.port); 33 | }); 34 | 35 | test.after('cleanup', async () => { 36 | await s.close(); 37 | }); 38 | 39 | test('url is required', async t => { 40 | await t.throwsAsync( 41 | got(), 42 | { 43 | message: 'Parameter `url` must be a string or object, not undefined' 44 | } 45 | ); 46 | }); 47 | 48 | test('url should be utf-8 encoded', async t => { 49 | await t.throwsAsync( 50 | got(`${s.url}/%D2%E0%EB%EB%E8%ED`), 51 | { 52 | message: 'URI malformed' 53 | } 54 | ); 55 | }); 56 | 57 | test('throws an error if the protocol is not specified', async t => { 58 | await t.throwsAsync(got('example.com'), TypeError); 59 | }); 60 | 61 | test('string url with searchParams is preserved', async t => { 62 | const path = '/?test=http://example.com?foo=bar'; 63 | const {body} = await got(`${s.url}${path}`); 64 | t.is(body, path); 65 | }); 66 | 67 | test('options are optional', async t => { 68 | t.is((await got(`${s.url}/test`)).body, '/test'); 69 | }); 70 | 71 | test('methods are normalized', async t => { 72 | const instance = got.create({ 73 | methods: got.defaults.methods, 74 | options: got.defaults.options, 75 | handler: (options, next) => { 76 | if (options.method === options.method.toUpperCase()) { 77 | t.pass(); 78 | } else { 79 | t.fail(); 80 | } 81 | 82 | return next(options); 83 | } 84 | }); 85 | 86 | await instance(`${s.url}/test`, {method: 'post'}); 87 | }); 88 | 89 | test('accepts url.parse object as first argument', async t => { 90 | t.is((await got(parse(`${s.url}/test`))).body, '/test'); 91 | }); 92 | 93 | test('requestUrl with url.parse object as first argument', async t => { 94 | t.is((await got(parse(`${s.url}/test`))).requestUrl, `${s.url}/test`); 95 | }); 96 | 97 | test('overrides searchParams from options', async t => { 98 | const {body} = await got( 99 | `${s.url}/?drop=this`, 100 | { 101 | searchParams: { 102 | test: 'wow' 103 | }, 104 | cache: { 105 | get(key) { 106 | t.is(key, `cacheable-request:GET:${s.url}/?test=wow`); 107 | }, 108 | set(key) { 109 | t.is(key, `cacheable-request:GET:${s.url}/?test=wow`); 110 | } 111 | } 112 | } 113 | ); 114 | 115 | t.is(body, '/?test=wow'); 116 | }); 117 | 118 | test('escapes searchParams parameter values', async t => { 119 | const {body} = await got(`${s.url}`, { 120 | searchParams: { 121 | test: 'it’s ok' 122 | } 123 | }); 124 | 125 | t.is(body, '/?test=it%E2%80%99s+ok'); 126 | }); 127 | 128 | test('the `searchParams` option can be a URLSearchParams', async t => { 129 | const searchParams = new URLSearchParams({test: 'wow'}); 130 | const {body} = await got(s.url, {searchParams}); 131 | t.is(body, '/?test=wow'); 132 | }); 133 | 134 | test('should ignore empty searchParams object', async t => { 135 | t.is((await got(`${s.url}/test`, {searchParams: {}})).requestUrl, `${s.url}/test`); 136 | }); 137 | 138 | test('should throw on invalid type of body', async t => { 139 | await t.throwsAsync(got(`${s.url}/`, {body: false}), TypeError); 140 | }); 141 | 142 | test('WHATWG URL support', async t => { 143 | const wURL = new URL(`${s.url}/test`); 144 | await t.notThrowsAsync(got(wURL)); 145 | }); 146 | 147 | test('should return streams when using stream option', async t => { 148 | const data = await pEvent(got(`${s.url}/stream`, {stream: true}), 'data'); 149 | t.is(data.toString(), 'ok'); 150 | }); 151 | 152 | test('accepts `url` as an option', async t => { 153 | await t.notThrowsAsync(got({url: `${s.url}/test`})); 154 | }); 155 | 156 | test('throws TypeError when `hooks` is not an object', async t => { 157 | await t.throwsAsync( 158 | () => got(s.url, {hooks: 'not object'}), 159 | { 160 | instanceOf: TypeError, 161 | message: 'Parameter `hooks` must be an object, not string' 162 | } 163 | ); 164 | }); 165 | 166 | test('throws TypeError when known `hooks` value is not an array', async t => { 167 | await t.throwsAsync( 168 | () => got(s.url, {hooks: {beforeRequest: {}}}), 169 | { 170 | instanceOf: TypeError, 171 | message: 'options.hooks.beforeRequest is not iterable' 172 | } 173 | ); 174 | }); 175 | 176 | test('throws TypeError when known `hooks` array item is not a function', async t => { 177 | await t.throwsAsync( 178 | () => got(s.url, {hooks: {beforeRequest: [{}]}}), 179 | { 180 | instanceOf: TypeError, 181 | message: 'hook is not a function' 182 | } 183 | ); 184 | }); 185 | 186 | test('allows extra keys in `hooks`', async t => { 187 | await t.notThrowsAsync(() => got(`${s.url}/test`, {hooks: {extra: {}}})); 188 | }); 189 | 190 | test('baseUrl works', async t => { 191 | const instanceA = got.extend({baseUrl: `${s.url}/test`}); 192 | const {body} = await instanceA('/foobar'); 193 | t.is(body, '/test/foobar'); 194 | }); 195 | 196 | test('accepts WHATWG URL as the baseUrl option', async t => { 197 | const instanceA = got.extend({baseUrl: new URL(`${s.url}/test`)}); 198 | const {body} = await instanceA('/foobar'); 199 | t.is(body, '/test/foobar'); 200 | }); 201 | 202 | test('backslash in the end of `baseUrl` is optional', async t => { 203 | const instanceA = got.extend({baseUrl: `${s.url}/test/`}); 204 | const {body} = await instanceA('/foobar'); 205 | t.is(body, '/test/foobar'); 206 | }); 207 | 208 | test('backslash in the beginning of `url` is optional when using baseUrl', async t => { 209 | const instanceA = got.extend({baseUrl: `${s.url}/test`}); 210 | const {body} = await instanceA('foobar'); 211 | t.is(body, '/test/foobar'); 212 | }); 213 | 214 | test('throws when trying to modify baseUrl after options got normalized', async t => { 215 | const instanceA = got.create({ 216 | methods: [], 217 | options: {baseUrl: 'https://example.com'}, 218 | handler: options => { 219 | options.baseUrl = 'https://google.com'; 220 | } 221 | }); 222 | 223 | await t.throwsAsync(instanceA('/'), 'Failed to set baseUrl. Options are normalized already.'); 224 | }); 225 | 226 | test('throws if the searchParams key is invalid', async t => { 227 | await t.throwsAsync(() => got(s.url, { 228 | searchParams: { 229 | // @ts-ignore 230 | [[]]: [] 231 | } 232 | }), TypeError); 233 | }); 234 | 235 | test('throws if the searchParams value is invalid', async t => { 236 | await t.throwsAsync(() => got(s.url, { 237 | searchParams: { 238 | foo: [] 239 | } 240 | }), TypeError); 241 | }); 242 | -------------------------------------------------------------------------------- /test/redirects.ts: -------------------------------------------------------------------------------- 1 | import {URL} from 'url'; 2 | import test from 'ava'; 3 | import nock from 'nock'; 4 | import got from '../source'; 5 | import {createServer, createSSLServer} from './helpers/server'; 6 | 7 | let http; 8 | let https; 9 | 10 | test.before('setup', async () => { 11 | const reached = (request, response) => { 12 | response.end('reached'); 13 | }; 14 | 15 | https = await createSSLServer(); 16 | http = await createServer(); 17 | 18 | // HTTPS Handlers 19 | 20 | https.on('/', (request, response) => { 21 | response.end('https'); 22 | }); 23 | 24 | https.on('/httpsToHttp', (request, response) => { 25 | response.writeHead(302, { 26 | location: http.url 27 | }); 28 | response.end(); 29 | }); 30 | 31 | // HTTP Handlers 32 | 33 | http.on('/', reached); 34 | 35 | http.on('/finite', (request, response) => { 36 | response.writeHead(302, { 37 | location: `${http.url}/` 38 | }); 39 | response.end(); 40 | }); 41 | 42 | http.on('/utf8-url-áé', reached); 43 | http.on('/?test=it’s+ok', reached); 44 | 45 | http.on('/redirect-with-utf8-binary', (request, response) => { 46 | response.writeHead(302, { 47 | location: Buffer.from((new URL('/utf8-url-áé', http.url)).toString(), 'utf8').toString('binary') 48 | }); 49 | response.end(); 50 | }); 51 | 52 | http.on('/redirect-with-uri-encoded-location', (request, response) => { 53 | response.writeHead(302, { 54 | location: new URL('/?test=it’s+ok', http.url).toString() 55 | }); 56 | response.end(); 57 | }); 58 | 59 | http.on('/endless', (request, response) => { 60 | response.writeHead(302, { 61 | location: `${http.url}/endless` 62 | }); 63 | response.end(); 64 | }); 65 | 66 | http.on('/relative', (request, response) => { 67 | response.writeHead(302, { 68 | location: '/' 69 | }); 70 | response.end(); 71 | }); 72 | 73 | http.on('/seeOther', (request, response) => { 74 | response.writeHead(303, { 75 | location: '/' 76 | }); 77 | response.end(); 78 | }); 79 | 80 | http.on('/temporary', (request, response) => { 81 | response.writeHead(307, { 82 | location: '/' 83 | }); 84 | response.end(); 85 | }); 86 | 87 | http.on('/permanent', (request, response) => { 88 | response.writeHead(308, { 89 | location: '/' 90 | }); 91 | response.end(); 92 | }); 93 | 94 | http.on('/relativeSearchParam?bang', (request, response) => { 95 | response.writeHead(302, { 96 | location: '/' 97 | }); 98 | response.end(); 99 | }); 100 | 101 | http.on('/httpToHttps', (request, response) => { 102 | response.writeHead(302, { 103 | location: https.url 104 | }); 105 | response.end(); 106 | }); 107 | 108 | http.on('/malformedRedirect', (request, response) => { 109 | response.writeHead(302, { 110 | location: '/%D8' 111 | }); 112 | response.end(); 113 | }); 114 | 115 | http.on('/invalidRedirect', (request, response) => { 116 | response.writeHead(302, { 117 | location: 'http://' 118 | }); 119 | response.end(); 120 | }); 121 | 122 | await http.listen(http.port); 123 | await https.listen(https.port); 124 | }); 125 | 126 | test.after('cleanup', async () => { 127 | await http.close(); 128 | await https.close(); 129 | }); 130 | 131 | test('follows redirect', async t => { 132 | const {body, redirectUrls} = await got(`${http.url}/finite`); 133 | t.is(body, 'reached'); 134 | t.deepEqual(redirectUrls, [`${http.url}/`]); 135 | }); 136 | 137 | test('follows 307, 308 redirect', async t => { 138 | const tempBody = (await got(`${http.url}/temporary`)).body; 139 | t.is(tempBody, 'reached'); 140 | 141 | const permBody = (await got(`${http.url}/permanent`)).body; 142 | t.is(permBody, 'reached'); 143 | }); 144 | 145 | test('does not follow redirect when disabled', async t => { 146 | t.is((await got(`${http.url}/finite`, {followRedirect: false})).statusCode, 302); 147 | }); 148 | 149 | test('relative redirect works', async t => { 150 | t.is((await got(`${http.url}/relative`)).body, 'reached'); 151 | }); 152 | 153 | test('throws on endless redirect', async t => { 154 | const error = await t.throwsAsync(got(`${http.url}/endless`)); 155 | t.is(error.message, 'Redirected 10 times. Aborting.'); 156 | // @ts-ignore 157 | t.deepEqual(error.redirectUrls, new Array(10).fill(`${http.url}/endless`)); 158 | }); 159 | 160 | test('searchParams in options are not breaking redirects', async t => { 161 | t.is((await got(`${http.url}/relativeSearchParam`, {searchParams: 'bang'})).body, 'reached'); 162 | }); 163 | 164 | test('hostname+path in options are not breaking redirects', async t => { 165 | t.is((await got(`${http.url}/relative`, { 166 | hostname: http.host, 167 | path: '/relative' 168 | })).body, 'reached'); 169 | }); 170 | 171 | test('redirect only GET and HEAD requests', async t => { 172 | const error = await t.throwsAsync(got.post(`${http.url}/relative`, {body: 'wow'})); 173 | t.is(error.message, 'Response code 302 (Found)'); 174 | // @ts-ignore 175 | t.is(error.path, '/relative'); 176 | // @ts-ignore 177 | t.is(error.statusCode, 302); 178 | }); 179 | 180 | test('redirect on 303 response even with post, put, delete', async t => { 181 | const {url, body} = await got.post(`${http.url}/seeOther`, {body: 'wow'}); 182 | t.is(url, `${http.url}/`); 183 | t.is(body, 'reached'); 184 | }); 185 | 186 | test('redirects from http to https works', async t => { 187 | t.truthy((await got(`${http.url}/httpToHttps`, {rejectUnauthorized: false})).body); 188 | }); 189 | 190 | test('redirects from https to http works', async t => { 191 | t.truthy((await got(`${https.url}/httpsToHttp`, {rejectUnauthorized: false})).body); 192 | }); 193 | 194 | test('redirects works with lowercase method', async t => { 195 | const {body} = (await got(`${http.url}/relative`, {method: 'head'})); 196 | t.is(body, ''); 197 | }); 198 | 199 | test('redirect response contains new url', async t => { 200 | const {url} = (await got(`${http.url}/finite`)); 201 | t.is(url, `${http.url}/`); 202 | }); 203 | 204 | test('redirect response contains old url', async t => { 205 | const {requestUrl} = (await got(`${http.url}/finite`)); 206 | t.is(requestUrl, `${http.url}/finite`); 207 | }); 208 | 209 | test('redirect response contains UTF-8 with binary encoding', async t => { 210 | t.is((await got(`${http.url}/redirect-with-utf8-binary`)).body, 'reached'); 211 | }); 212 | 213 | test('redirect response contains UTF-8 with URI encoding', async t => { 214 | t.is((await got(`${http.url}/redirect-with-uri-encoded-location`)).body, 'reached'); 215 | }); 216 | 217 | test('throws on malformed redirect URI', async t => { 218 | const error = await t.throwsAsync(got(`${http.url}/malformedRedirect`)); 219 | t.is(error.name, 'URIError'); 220 | }); 221 | 222 | test('throws on invalid redirect URL', async t => { 223 | const error = await t.throwsAsync(got(`${http.url}/invalidRedirect`)); 224 | // @ts-ignore 225 | t.is(error.code, 'ERR_INVALID_URL'); 226 | }); 227 | 228 | test('port is reset on redirect', async t => { 229 | const server = await createServer(); 230 | server.on('/', (request, response) => { 231 | response.writeHead(307, { 232 | location: 'http://localhost' 233 | }); 234 | response.end(); 235 | }); 236 | await server.listen(server.port); 237 | 238 | nock('http://localhost').get('/').reply(200, 'ok'); 239 | 240 | const {body} = await got(server.url); 241 | t.is(body, 'ok'); 242 | 243 | await server.close(); 244 | }); 245 | -------------------------------------------------------------------------------- /test/retry.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import test from 'ava'; 3 | import pEvent from 'p-event'; 4 | import got from '../source'; 5 | import {createServer} from './helpers/server'; 6 | 7 | let s; 8 | let trys = 0; 9 | let knocks = 0; 10 | let fifth = 0; 11 | let lastTried413access = Date.now(); 12 | let lastTried413TimestampAccess; 13 | 14 | const retryAfterOn413 = 2; 15 | const socketTimeout = 200; 16 | 17 | test.before('setup', async () => { 18 | s = await createServer(); 19 | 20 | s.on('/long', () => {}); 21 | 22 | s.on('/knock-twice', (request, response) => { 23 | if (knocks++ === 1) { 24 | response.end('who`s there?'); 25 | } 26 | }); 27 | 28 | s.on('/try-me', () => { 29 | trys++; 30 | }); 31 | 32 | s.on('/fifth', (request, response) => { 33 | if (fifth++ === 5) { 34 | response.end('who`s there?'); 35 | } 36 | }); 37 | 38 | s.on('/500', (request, response) => { 39 | response.statusCode = 500; 40 | response.end(); 41 | }); 42 | 43 | s.on('/measure413', (request, response) => { 44 | response.writeHead(413, { 45 | 'Retry-After': retryAfterOn413 46 | }); 47 | response.end((Date.now() - lastTried413access).toString()); 48 | 49 | lastTried413access = Date.now(); 50 | }); 51 | 52 | s.on('/413', (request, response) => { 53 | response.writeHead(413, { 54 | 'Retry-After': retryAfterOn413 55 | }); 56 | response.end(); 57 | }); 58 | 59 | s.on('/413withTimestamp', (request, response) => { 60 | const date = (new Date(Date.now() + (retryAfterOn413 * 1000))).toUTCString(); 61 | 62 | response.writeHead(413, { 63 | 'Retry-After': date 64 | }); 65 | response.end(lastTried413TimestampAccess); 66 | lastTried413TimestampAccess = date; 67 | }); 68 | 69 | s.on('/413withoutRetryAfter', (request, response) => { 70 | response.statusCode = 413; 71 | response.end(); 72 | }); 73 | 74 | s.on('/503', (request, response) => { 75 | response.statusCode = 503; 76 | response.end(); 77 | }); 78 | 79 | await s.listen(s.port); 80 | }); 81 | 82 | test.after('cleanup', async () => { 83 | await s.close(); 84 | }); 85 | 86 | test('works on timeout error', async t => { 87 | t.is((await got(`${s.url}/knock-twice`, {timeout: {socket: socketTimeout}})).body, 'who`s there?'); 88 | }); 89 | 90 | test('can be disabled with option', async t => { 91 | const error = await t.throwsAsync(got(`${s.url}/try-me`, { 92 | timeout: {socket: socketTimeout}, 93 | retry: 0 94 | })); 95 | t.truthy(error); 96 | t.is(trys, 1); 97 | }); 98 | 99 | test('function gets iter count', async t => { 100 | await got(`${s.url}/fifth`, { 101 | timeout: {socket: socketTimeout}, 102 | retry: { 103 | retries: iteration => iteration < 10 104 | } 105 | }); 106 | t.is(fifth, 6); 107 | }); 108 | 109 | test('falsy value prevents retries', async t => { 110 | const error = await t.throwsAsync(got(`${s.url}/long`, { 111 | timeout: {socket: socketTimeout}, 112 | retry: { 113 | retries: () => 0 114 | } 115 | })); 116 | t.truthy(error); 117 | }); 118 | 119 | test('falsy value prevents retries #2', async t => { 120 | const error = await t.throwsAsync(got(`${s.url}/long`, { 121 | timeout: {socket: socketTimeout}, 122 | retry: { 123 | retries: (iter, error) => { 124 | t.truthy(error); 125 | return false; 126 | } 127 | } 128 | })); 129 | t.truthy(error); 130 | }); 131 | 132 | test('custom retries', async t => { 133 | let tried = false; 134 | const error = await t.throwsAsync(got(`${s.url}/500`, { 135 | throwHttpErrors: true, 136 | retry: { 137 | retries: iter => { 138 | if (iter === 1) { 139 | tried = true; 140 | return 1; 141 | } 142 | 143 | return 0; 144 | }, methods: [ 145 | 'GET' 146 | ], statusCodes: [ 147 | 500 148 | ] 149 | } 150 | })); 151 | // @ts-ignore 152 | t.is(error.statusCode, 500); 153 | t.true(tried); 154 | }); 155 | 156 | test('custom errors', async t => { 157 | const errorCode = 'OH_SNAP'; 158 | 159 | let isTried = false; 160 | const error = await t.throwsAsync(got(`${s.url}/500`, { 161 | request: (...args) => { 162 | // @ts-ignore 163 | const request = http.request(...args); 164 | if (!isTried) { 165 | isTried = true; 166 | const error = new Error('Snap!'); 167 | // @ts-ignore 168 | error.code = errorCode; 169 | 170 | setTimeout(() => request.emit('error', error)); 171 | } 172 | 173 | return request; 174 | }, 175 | retry: { 176 | retries: 1, 177 | methods: [ 178 | 'GET' 179 | ], 180 | errorCodes: [ 181 | errorCode 182 | ] 183 | } 184 | })); 185 | 186 | // @ts-ignore 187 | t.is(error.statusCode, 500); 188 | t.true(isTried); 189 | }); 190 | 191 | test('respect 413 Retry-After', async t => { 192 | const {statusCode, body} = await got(`${s.url}/measure413`, { 193 | throwHttpErrors: false, 194 | retry: 1 195 | }); 196 | t.is(statusCode, 413); 197 | t.true(Number(body) >= retryAfterOn413 * 1000); 198 | }); 199 | 200 | test('respect 413 Retry-After with RFC-1123 timestamp', async t => { 201 | const {statusCode, body} = await got(`${s.url}/413withTimestamp`, { 202 | throwHttpErrors: false, 203 | retry: 1 204 | }); 205 | t.is(statusCode, 413); 206 | t.true(Date.now() >= Date.parse(body)); 207 | }); 208 | 209 | test('doesn\'t retry on 413 with empty statusCodes and methods', async t => { 210 | const {statusCode, retryCount} = await got(`${s.url}/413`, { 211 | throwHttpErrors: false, 212 | retry: { 213 | retries: 1, 214 | statusCodes: [], 215 | methods: [] 216 | } 217 | }); 218 | t.is(statusCode, 413); 219 | t.is(retryCount, 0); 220 | }); 221 | 222 | test('doesn\'t retry on 413 with empty methods', async t => { 223 | const {statusCode, retryCount} = await got(`${s.url}/413`, { 224 | throwHttpErrors: false, 225 | retry: { 226 | retries: 1, 227 | statusCodes: [413], 228 | methods: [] 229 | } 230 | }); 231 | t.is(statusCode, 413); 232 | t.is(retryCount, 0); 233 | }); 234 | 235 | test('doesn\'t retry on 413 without Retry-After header', async t => { 236 | const {retryCount} = await got(`${s.url}/413withoutRetryAfter`, { 237 | throwHttpErrors: false 238 | }); 239 | t.is(retryCount, 0); 240 | }); 241 | 242 | test('retries on 503 without Retry-After header', async t => { 243 | const {retryCount} = await got(`${s.url}/503`, { 244 | throwHttpErrors: false, 245 | retry: 1 246 | }); 247 | t.is(retryCount, 1); 248 | }); 249 | 250 | test('doesn\'t retry on streams', async t => { 251 | const stream = got.stream(s.url, { 252 | timeout: 1, 253 | retry: { 254 | retries: () => { 255 | t.fail('Retries on streams'); 256 | } 257 | } 258 | }); 259 | await t.throwsAsync(pEvent(stream, 'response')); 260 | }); 261 | 262 | test('doesn\'t retry if Retry-After header is greater than maxRetryAfter', async t => { 263 | const {retryCount} = await got(`${s.url}/413`, { 264 | retry: {maxRetryAfter: 1000}, 265 | throwHttpErrors: false 266 | }); 267 | t.is(retryCount, 0); 268 | }); 269 | 270 | test('doesn\'t retry when set to false', async t => { 271 | const {statusCode, retryCount} = await got(`${s.url}/413`, { 272 | throwHttpErrors: false, 273 | retry: false 274 | }); 275 | t.is(statusCode, 413); 276 | t.is(retryCount, 0); 277 | }); 278 | 279 | test('works when defaults.options.retry is not an object', async t => { 280 | const instance = got.extend({ 281 | retry: 2 282 | }); 283 | 284 | const {retryCount} = await instance(`${s.url}/413`, { 285 | throwHttpErrors: false 286 | }); 287 | t.is(retryCount, 0); 288 | }); 289 | 290 | test('retry function can throw', async t => { 291 | const error = 'Simple error'; 292 | await t.throwsAsync(got(`${s.url}/413`, { 293 | retry: { 294 | retries: () => { 295 | throw new Error(error); 296 | } 297 | } 298 | }), error); 299 | }); 300 | -------------------------------------------------------------------------------- /advanced-creation.md: -------------------------------------------------------------------------------- 1 | # Advanced creation 2 | 3 | > Make calling REST APIs easier by creating niche-specific `got` instances. 4 | 5 | #### got.create(settings) 6 | 7 | Example: [gh-got](https://github.com/sindresorhus/gh-got/blob/master/index.js) 8 | 9 | Configure a new `got` instance with the provided settings. You can access the resolved options with the `.defaults` property on the instance. 10 | 11 | **Note:** In contrast to `got.extend()`, this method has no defaults. 12 | 13 | ##### [options](readme.md#options) 14 | 15 | To inherit from parent, set it as `got.defaults.options` or use [`got.mergeOptions(defaults.options, options)`](readme.md#gotmergeoptionsparentoptions-newoptions).
16 | **Note**: Avoid using [object spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals) as it doesn't work recursively. 17 | 18 | ##### mutableDefaults 19 | 20 | Type: `boolean`
21 | Default: `false` 22 | 23 | States if the defaults are mutable. It's very useful when you need to [update headers over time](readme.md#hooksafterresponse). 24 | 25 | ##### handler 26 | 27 | Type: `Function`
28 | Default: `undefined` 29 | 30 | A function making additional changes to the request. 31 | 32 | To inherit from parent, set it as `got.defaults.handler`.
33 | To use the default handler, just omit specifying this. 34 | 35 | ###### [options](readme.md#options) 36 | 37 | **Note:** These options are [normalized](source/normalize-arguments.js). 38 | 39 | ###### next() 40 | 41 | Returns a `Promise` or a `Stream` depending on [`options.stream`](readme.md#stream). 42 | 43 | ```js 44 | const settings = { 45 | handler: (options, next) => { 46 | if (options.stream) { 47 | // It's a Stream 48 | // We can perform stream-specific actions on it 49 | return next(options) 50 | .on('request', request => setTimeout(() => request.abort(), 50)); 51 | } 52 | 53 | // It's a Promise 54 | return next(options); 55 | }, 56 | options: got.mergeOptions(got.defaults.options, { 57 | responseType: 'json' 58 | }) 59 | }; 60 | 61 | const jsonGot = got.create(settings); 62 | ``` 63 | 64 | ```js 65 | const defaults = { 66 | options: { 67 | method: 'GET', 68 | retry: { 69 | retries: 2, 70 | methods: [ 71 | 'GET', 72 | 'PUT', 73 | 'HEAD', 74 | 'DELETE', 75 | 'OPTIONS', 76 | 'TRACE' 77 | ], 78 | statusCodes: [ 79 | 408, 80 | 413, 81 | 429, 82 | 500, 83 | 502, 84 | 503, 85 | 504 86 | ], 87 | errorCodes: [ 88 | 'ETIMEDOUT', 89 | 'ECONNRESET', 90 | 'EADDRINUSE', 91 | 'ECONNREFUSED', 92 | 'EPIPE', 93 | 'ENOTFOUND', 94 | 'ENETUNREACH', 95 | 'EAI_AGAIN' 96 | ] 97 | }, 98 | headers: { 99 | 'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)` 100 | }, 101 | hooks: { 102 | beforeError: [], 103 | init: [], 104 | beforeRequest: [], 105 | beforeRedirect: [], 106 | beforeRetry: [], 107 | afterResponse: [] 108 | }, 109 | decompress: true, 110 | throwHttpErrors: true, 111 | followRedirect: true, 112 | stream: false, 113 | form: false, 114 | cache: false, 115 | useElectronNet: false, 116 | responseType: 'text', 117 | resolveBodyOnly: 'false' 118 | }, 119 | mutableDefaults: false 120 | }; 121 | 122 | // Same as: 123 | const defaults = { 124 | handler: got.defaults.handler, 125 | options: got.defaults.options, 126 | mutableDefaults: got.defaults.mutableDefaults 127 | }; 128 | 129 | const unchangedGot = got.create(defaults); 130 | ``` 131 | 132 | ```js 133 | const settings = { 134 | handler: got.defaults.handler, 135 | options: got.mergeOptions(got.defaults.options, { 136 | headers: { 137 | unicorn: 'rainbow' 138 | } 139 | }) 140 | }; 141 | 142 | const unicorn = got.create(settings); 143 | 144 | // Same as: 145 | const unicorn = got.extend({headers: {unicorn: 'rainbow'}}); 146 | ``` 147 | 148 | ### Merging instances 149 | 150 | Got supports composing multiple instances together. This is very powerful. You can create a client that limits download speed and then compose it with an instance that signs a request. It's like plugins without any of the plugin mess. You just create instances and then compose them together. 151 | 152 | #### got.mergeInstances(instanceA, instanceB, ...) 153 | 154 | Merges many instances into a single one: 155 | - options are merged using [`got.mergeOptions()`](readme.md#gotmergeoptionsparentoptions-newoptions) (+ hooks are merged too), 156 | - handlers are stored in an array. 157 | 158 | ## Examples 159 | 160 | Some examples of what kind of instances you could compose together: 161 | 162 | #### Denying redirects that lead to other sites than specified 163 | 164 | ```js 165 | const controlRedirects = got.create({ 166 | options: got.defaults.options, 167 | handler: (options, next) => { 168 | const promiseOrStream = next(options); 169 | return promiseOrStream.on('redirect', resp => { 170 | const host = new URL(resp.url).host; 171 | if (options.allowedHosts && !options.allowedHosts.includes(host)) { 172 | promiseOrStream.cancel(`Redirection to ${host} is not allowed`); 173 | } 174 | }); 175 | } 176 | }); 177 | ``` 178 | 179 | #### Limiting download & upload 180 | 181 | It's very useful in case your machine's got a little amount of RAM. 182 | 183 | ```js 184 | const limitDownloadUpload = got.create({ 185 | options: got.defaults.options, 186 | handler: (options, next) => { 187 | let promiseOrStream = next(options); 188 | if (typeof options.downloadLimit === 'number') { 189 | promiseOrStream.on('downloadProgress', progress => { 190 | if (progress.transferred > options.downloadLimit && progress.percent !== 1) { 191 | promiseOrStream.cancel(`Exceeded the download limit of ${options.downloadLimit} bytes`); 192 | } 193 | }); 194 | } 195 | 196 | if (typeof options.uploadLimit === 'number') { 197 | promiseOrStream.on('uploadProgress', progress => { 198 | if (progress.transferred > options.uploadLimit && progress.percent !== 1) { 199 | promiseOrStream.cancel(`Exceeded the upload limit of ${options.uploadLimit} bytes`); 200 | } 201 | }); 202 | } 203 | 204 | return promiseOrStream; 205 | } 206 | }); 207 | ``` 208 | 209 | #### No user agent 210 | 211 | ```js 212 | const noUserAgent = got.extend({ 213 | headers: { 214 | 'user-agent': null 215 | } 216 | }); 217 | ``` 218 | 219 | #### Custom endpoint 220 | 221 | ```js 222 | const httpbin = got.extend({ 223 | baseUrl: 'https://httpbin.org/' 224 | }); 225 | ``` 226 | 227 | #### Signing requests 228 | 229 | ```js 230 | const crypto = require('crypto'); 231 | const getMessageSignature = (data, secret) => crypto.createHmac('sha256', secret).update(data).digest('hex').toUpperCase(); 232 | const signRequest = got.extend({ 233 | hooks: { 234 | beforeRequest: [ 235 | options => { 236 | options.headers['sign'] = getMessageSignature(options.body || '', process.env.SECRET); 237 | } 238 | ] 239 | } 240 | }); 241 | ``` 242 | 243 | #### Putting it all together 244 | 245 | If these instances are different modules and you don't want to rewrite them, use `got.mergeInstances()`. 246 | 247 | **Note**: The `noUserAgent` instance must be placed at the end of chain as the instances are merged in order. Other instances do have the `user-agent` header. 248 | 249 | ```js 250 | const merged = got.mergeInstances(controlRedirects, limitDownloadUpload, httpbin, signRequest, noUserAgent); 251 | 252 | (async () => { 253 | // There's no 'user-agent' header :) 254 | await merged('/'); 255 | /* HTTP Request => 256 | * GET / HTTP/1.1 257 | * accept-encoding: gzip, deflate, br 258 | * sign: F9E66E179B6747AE54108F82F8ADE8B3C25D76FD30AFDE6C395822C530196169 259 | * Host: httpbin.org 260 | * Connection: close 261 | */ 262 | 263 | const MEGABYTE = 1048576; 264 | await merged('http://ipv4.download.thinkbroadband.com/5MB.zip', {downloadLimit: MEGABYTE}); 265 | // CancelError: Exceeded the download limit of 1048576 bytes 266 | 267 | await merged('https://jigsaw.w3.org/HTTP/300/301.html', {allowedHosts: ['google.com']}); 268 | // CancelError: Redirection to jigsaw.w3.org is not allowed 269 | })(); 270 | ``` 271 | -------------------------------------------------------------------------------- /source/normalize-arguments.ts: -------------------------------------------------------------------------------- 1 | import urlLib, {URL, URLSearchParams} from 'url'; // TODO: Use the `URL` global when targeting Node.js 10 2 | import CacheableLookup from 'cacheable-lookup'; 3 | import is from '@sindresorhus/is'; 4 | import lowercaseKeys from 'lowercase-keys'; 5 | import urlToOptions from './utils/url-to-options'; 6 | import validateSearchParams from './utils/validate-search-params'; 7 | import supportsBrotli from './utils/supports-brotli'; 8 | import merge from './merge'; 9 | import knownHookEvents from './known-hook-events'; 10 | 11 | const retryAfterStatusCodes = new Set([413, 429, 503]); 12 | 13 | let shownDeprecation = false; 14 | 15 | // `preNormalize` handles static options (e.g. headers). 16 | // For example, when you create a custom instance and make a request 17 | // with no static changes, they won't be normalized again. 18 | // 19 | // `normalize` operates on dynamic options - they cannot be saved. 20 | // For example, `body` is everytime different per request. 21 | // When it's done normalizing the new options, it performs merge() 22 | // on the prenormalized options and the normalized ones. 23 | 24 | export const preNormalizeArguments = (options: any, defaults?: any) => { 25 | if (is.nullOrUndefined(options.headers)) { 26 | options.headers = {}; 27 | } else { 28 | options.headers = lowercaseKeys(options.headers); 29 | } 30 | 31 | if (options.baseUrl && !options.baseUrl.toString().endsWith('/')) { 32 | options.baseUrl += '/'; 33 | } 34 | 35 | if (is.nullOrUndefined(options.hooks)) { 36 | options.hooks = {}; 37 | } else if (!is.object(options.hooks)) { 38 | throw new TypeError(`Parameter \`hooks\` must be an object, not ${is(options.hooks)}`); 39 | } 40 | 41 | for (const event of knownHookEvents) { 42 | if (is.nullOrUndefined(options.hooks[event])) { 43 | if (defaults) { 44 | options.hooks[event] = [...defaults.hooks[event]]; 45 | } else { 46 | options.hooks[event] = []; 47 | } 48 | } 49 | } 50 | 51 | if (is.number(options.timeout)) { 52 | options.gotTimeout = {request: options.timeout}; 53 | } else if (is.object(options.timeout)) { 54 | options.gotTimeout = options.timeout; 55 | } 56 | 57 | delete options.timeout; 58 | 59 | const {retry} = options; 60 | options.retry = { 61 | retries: () => 0, 62 | methods: new Set(), 63 | statusCodes: new Set(), 64 | errorCodes: new Set(), 65 | maxRetryAfter: undefined 66 | }; 67 | 68 | if (is.nonEmptyObject(defaults) && retry !== false) { 69 | options.retry = {...defaults.retry}; 70 | } 71 | 72 | if (retry !== false) { 73 | if (is.number(retry)) { 74 | options.retry.retries = retry; 75 | } else { 76 | options.retry = {...options.retry, ...retry}; 77 | } 78 | } 79 | 80 | if (!options.retry.maxRetryAfter && options.gotTimeout) { 81 | options.retry.maxRetryAfter = Math.min(...[options.gotTimeout.request, options.gotTimeout.connection].filter(n => !is.nullOrUndefined(n))); 82 | } 83 | 84 | if (is.array(options.retry.methods)) { 85 | options.retry.methods = new Set(options.retry.methods.map(method => method.toUpperCase())); 86 | } 87 | 88 | if (is.array(options.retry.statusCodes)) { 89 | options.retry.statusCodes = new Set(options.retry.statusCodes); 90 | } 91 | 92 | if (is.array(options.retry.errorCodes)) { 93 | options.retry.errorCodes = new Set(options.retry.errorCodes); 94 | } 95 | 96 | if (options.dnsCache) { 97 | const cacheableLookup = new CacheableLookup({cacheAdapter: options.dnsCache}); 98 | options.lookup = cacheableLookup.lookup; 99 | delete options.dnsCache; 100 | } 101 | 102 | return options; 103 | }; 104 | 105 | export const normalizeArguments = (url, options, defaults?: any) => { 106 | if (is.plainObject(url)) { 107 | options = {...url, ...options}; 108 | url = options.url || {}; 109 | delete options.url; 110 | } 111 | 112 | if (defaults) { 113 | options = merge({}, defaults.options, options ? preNormalizeArguments(options, defaults.options) : {}); 114 | } else { 115 | options = merge({}, preNormalizeArguments(options)); 116 | } 117 | 118 | if (!is.string(url) && !is.object(url)) { 119 | throw new TypeError(`Parameter \`url\` must be a string or object, not ${is(url)}`); 120 | } 121 | 122 | if (is.string(url)) { 123 | if (options.baseUrl) { 124 | if (url.startsWith('/')) { 125 | url = url.slice(1); 126 | } 127 | } else { 128 | url = url.replace(/^unix:/, 'http://$&'); 129 | } 130 | 131 | url = urlToOptions(new URL(url, options.baseUrl)); 132 | } else if (is(url) === 'URL') { 133 | url = urlToOptions(url); 134 | } 135 | 136 | // Override both null/undefined with default protocol 137 | options = merge({path: ''}, url, {protocol: url.protocol || 'https:'}, options); 138 | 139 | for (const hook of options.hooks.init) { 140 | const called = hook(options); 141 | 142 | if (is.promise(called)) { 143 | throw new TypeError('The `init` hook must be a synchronous function'); 144 | } 145 | } 146 | 147 | const {baseUrl} = options; 148 | Object.defineProperty(options, 'baseUrl', { 149 | set: () => { 150 | throw new Error('Failed to set baseUrl. Options are normalized already.'); 151 | }, 152 | get: () => baseUrl 153 | }); 154 | 155 | let {searchParams} = options; 156 | delete options.searchParams; 157 | 158 | if (options.query) { 159 | if (!shownDeprecation) { 160 | console.warn('`options.query` is deprecated. We support it solely for compatibility - it will be removed in Got 11. Use `options.searchParams` instead.'); 161 | shownDeprecation = true; 162 | } 163 | 164 | searchParams = options.query; 165 | delete options.query; 166 | } 167 | 168 | // TODO: This should be used in the `options` type instead 169 | interface SearchParams { 170 | [key: string]: string | number | boolean | null; 171 | } 172 | 173 | if (is.nonEmptyString(searchParams) || is.nonEmptyObject(searchParams) || searchParams instanceof URLSearchParams) { 174 | if (!is.string(searchParams)) { 175 | if (!(searchParams instanceof URLSearchParams)) { 176 | validateSearchParams(searchParams); 177 | searchParams = searchParams as SearchParams; 178 | } 179 | 180 | searchParams = (new URLSearchParams(searchParams)).toString(); 181 | } 182 | 183 | options.path = `${options.path.split('?')[0]}?${searchParams}`; 184 | } 185 | 186 | if (options.hostname === 'unix') { 187 | const matches = /(.+?):(.+)/.exec(options.path); 188 | 189 | if (matches) { 190 | const [, socketPath, path] = matches; 191 | options = { 192 | ...options, 193 | socketPath, 194 | path, 195 | host: null 196 | }; 197 | } 198 | } 199 | 200 | const {headers} = options; 201 | for (const [key, value] of Object.entries(headers)) { 202 | if (is.nullOrUndefined(value)) { 203 | delete headers[key]; 204 | } 205 | } 206 | 207 | if (options.decompress && is.undefined(headers['accept-encoding'])) { 208 | headers['accept-encoding'] = supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate'; 209 | } 210 | 211 | if (options.method) { 212 | options.method = options.method.toUpperCase(); 213 | } 214 | 215 | if (!is.function_(options.retry.retries)) { 216 | const {retries} = options.retry; 217 | 218 | options.retry.retries = (iteration, error) => { 219 | if (iteration > retries) { 220 | return 0; 221 | } 222 | 223 | if ((!error || !options.retry.errorCodes.has(error.code)) && (!options.retry.methods.has(error.method) || !options.retry.statusCodes.has(error.statusCode))) { 224 | return 0; 225 | } 226 | 227 | if (Reflect.has(error, 'headers') && Reflect.has(error.headers, 'retry-after') && retryAfterStatusCodes.has(error.statusCode)) { 228 | let after = Number(error.headers['retry-after']); 229 | if (is.nan(after)) { 230 | after = Date.parse(error.headers['retry-after']) - Date.now(); 231 | } else { 232 | after *= 1000; 233 | } 234 | 235 | if (after > options.retry.maxRetryAfter) { 236 | return 0; 237 | } 238 | 239 | return after; 240 | } 241 | 242 | if (error.statusCode === 413) { 243 | return 0; 244 | } 245 | 246 | const noise = Math.random() * 100; 247 | return ((2 ** (iteration - 1)) * 1000) + noise; 248 | }; 249 | } 250 | 251 | return options; 252 | }; 253 | 254 | export const reNormalizeArguments = options => normalizeArguments(urlLib.format(options), options); 255 | -------------------------------------------------------------------------------- /test/hooks.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import delay from 'delay'; 3 | import getStream from 'get-stream'; 4 | import got from '../source'; 5 | import {createServer} from './helpers/server'; 6 | 7 | const errorString = 'oops'; 8 | const error = new Error(errorString); 9 | let s; 10 | 11 | let visited401then500; 12 | 13 | test.before('setup', async () => { 14 | s = await createServer(); 15 | const echoHeaders = (request, response) => { 16 | response.statusCode = 200; 17 | response.write(JSON.stringify(request.headers)); 18 | response.end(); 19 | }; 20 | 21 | s.on('/', echoHeaders); 22 | s.on('/body', async (request, response) => { 23 | response.end(await getStream(request)); 24 | }); 25 | s.on('/redirect', (request, response) => { 26 | response.statusCode = 302; 27 | response.setHeader('location', '/'); 28 | response.end(); 29 | }); 30 | s.on('/retry', (request, response) => { 31 | if (request.headers.foo) { 32 | response.statusCode = 302; 33 | response.setHeader('location', '/'); 34 | response.end(); 35 | } 36 | 37 | response.statusCode = 500; 38 | response.end(); 39 | }); 40 | 41 | s.on('/401', (request, response) => { 42 | if (request.headers.token !== 'unicorn') { 43 | response.statusCode = 401; 44 | } 45 | 46 | response.end(); 47 | }); 48 | 49 | s.on('/401then500', (request, response) => { 50 | if (visited401then500) { 51 | response.statusCode = 500; 52 | } else { 53 | visited401then500 = true; 54 | response.statusCode = 401; 55 | } 56 | 57 | response.end(); 58 | }); 59 | 60 | await s.listen(s.port); 61 | }); 62 | 63 | test.after('cleanup', async () => { 64 | await s.close(); 65 | }); 66 | 67 | test('async hooks', async t => { 68 | const {body} = await got(s.url, { 69 | responseType: 'json', 70 | hooks: { 71 | beforeRequest: [ 72 | async options => { 73 | await delay(100); 74 | options.headers.foo = 'bar'; 75 | } 76 | ] 77 | } 78 | }); 79 | t.is(body.foo, 'bar'); 80 | }); 81 | 82 | test('catches init thrown errors', async t => { 83 | await t.throwsAsync(() => got(s.url, { 84 | hooks: { 85 | init: [() => { 86 | throw error; 87 | }] 88 | } 89 | }), errorString); 90 | }); 91 | 92 | test('catches beforeRequest thrown errors', async t => { 93 | await t.throwsAsync(() => got(s.url, { 94 | hooks: { 95 | beforeRequest: [() => { 96 | throw error; 97 | }] 98 | } 99 | }), errorString); 100 | }); 101 | 102 | test('catches beforeRedirect thrown errors', async t => { 103 | await t.throwsAsync(() => got(`${s.url}/redirect`, { 104 | hooks: { 105 | beforeRedirect: [() => { 106 | throw error; 107 | }] 108 | } 109 | }), errorString); 110 | }); 111 | 112 | test('catches beforeRetry thrown errors', async t => { 113 | await t.throwsAsync(() => got(`${s.url}/retry`, { 114 | hooks: { 115 | beforeRetry: [() => { 116 | throw error; 117 | }] 118 | } 119 | }), errorString); 120 | }); 121 | 122 | test('catches afterResponse thrown errors', async t => { 123 | await t.throwsAsync(() => got(s.url, { 124 | hooks: { 125 | afterResponse: [() => { 126 | throw error; 127 | }] 128 | } 129 | }), errorString); 130 | }); 131 | 132 | test('throws a helpful error when passing async function as init hook', async t => { 133 | await t.throwsAsync(() => got(s.url, { 134 | hooks: { 135 | init: [() => Promise.resolve()] 136 | } 137 | }), 'The `init` hook must be a synchronous function'); 138 | }); 139 | 140 | test('catches beforeRequest promise rejections', async t => { 141 | await t.throwsAsync(() => got(s.url, { 142 | hooks: { 143 | beforeRequest: [() => Promise.reject(error)] 144 | } 145 | }), errorString); 146 | }); 147 | 148 | test('catches beforeRedirect promise rejections', async t => { 149 | await t.throwsAsync(() => got(`${s.url}/redirect`, { 150 | hooks: { 151 | beforeRedirect: [() => Promise.reject(error)] 152 | } 153 | }), errorString); 154 | }); 155 | 156 | test('catches beforeRetry promise rejections', async t => { 157 | await t.throwsAsync(() => got(`${s.url}/retry`, { 158 | hooks: { 159 | beforeRetry: [() => Promise.reject(error)] 160 | } 161 | }), errorString); 162 | }); 163 | 164 | test('catches afterResponse promise rejections', async t => { 165 | await t.throwsAsync(() => got(s.url, { 166 | hooks: { 167 | afterResponse: [() => Promise.reject(error)] 168 | } 169 | }), errorString); 170 | }); 171 | 172 | test('catches beforeError errors', async t => { 173 | await t.throwsAsync(() => got(s.url, { 174 | request: () => {}, 175 | hooks: { 176 | beforeError: [() => Promise.reject(error)] 177 | } 178 | }), errorString); 179 | }); 180 | 181 | test('init is called with options', async t => { 182 | await got.post(s.url, { 183 | json: true, 184 | hooks: { 185 | init: [ 186 | options => { 187 | t.is(options.path, '/'); 188 | t.is(options.hostname, 'localhost'); 189 | } 190 | ] 191 | } 192 | }); 193 | }); 194 | 195 | test('init allows modifications', async t => { 196 | const {body} = await got(`${s.url}/body`, { 197 | hooks: { 198 | init: [ 199 | options => { 200 | options.method = 'POST'; 201 | options.body = 'foobar'; 202 | } 203 | ] 204 | } 205 | }); 206 | t.is(body, 'foobar'); 207 | }); 208 | 209 | test('beforeRequest is called with options', async t => { 210 | await got(s.url, { 211 | responseType: 'json', 212 | hooks: { 213 | beforeRequest: [ 214 | options => { 215 | t.is(options.path, '/'); 216 | t.is(options.hostname, 'localhost'); 217 | } 218 | ] 219 | } 220 | }); 221 | }); 222 | 223 | test('beforeRequest allows modifications', async t => { 224 | const {body} = await got(s.url, { 225 | responseType: 'json', 226 | hooks: { 227 | beforeRequest: [ 228 | options => { 229 | options.headers.foo = 'bar'; 230 | } 231 | ] 232 | } 233 | }); 234 | t.is(body.foo, 'bar'); 235 | }); 236 | 237 | test('beforeRedirect is called with options', async t => { 238 | await got(`${s.url}/redirect`, { 239 | responseType: 'json', 240 | hooks: { 241 | beforeRedirect: [ 242 | options => { 243 | t.is(options.path, '/'); 244 | t.is(options.hostname, 'localhost'); 245 | } 246 | ] 247 | } 248 | }); 249 | }); 250 | 251 | test('beforeRedirect allows modifications', async t => { 252 | const {body} = await got(`${s.url}/redirect`, { 253 | responseType: 'json', 254 | hooks: { 255 | beforeRedirect: [ 256 | options => { 257 | options.headers.foo = 'bar'; 258 | } 259 | ] 260 | } 261 | }); 262 | t.is(body.foo, 'bar'); 263 | }); 264 | 265 | test('beforeRetry is called with options', async t => { 266 | await got(`${s.url}/retry`, { 267 | responseType: 'json', 268 | retry: 1, 269 | throwHttpErrors: false, 270 | hooks: { 271 | beforeRetry: [ 272 | (options, error, retryCount) => { 273 | t.is(options.hostname, 'localhost'); 274 | t.truthy(error); 275 | t.true(retryCount >= 1); 276 | } 277 | ] 278 | } 279 | }); 280 | }); 281 | 282 | test('beforeRetry allows modifications', async t => { 283 | const {body} = await got(`${s.url}/retry`, { 284 | responseType: 'json', 285 | hooks: { 286 | beforeRetry: [ 287 | options => { 288 | options.headers.foo = 'bar'; 289 | } 290 | ] 291 | } 292 | }); 293 | t.is(body.foo, 'bar'); 294 | }); 295 | 296 | test('afterResponse is called with response', async t => { 297 | await got(`${s.url}`, { 298 | responseType: 'json', 299 | hooks: { 300 | afterResponse: [ 301 | response => { 302 | t.is(typeof response.body, 'string'); 303 | 304 | return response; 305 | } 306 | ] 307 | } 308 | }); 309 | }); 310 | 311 | test('afterResponse allows modifications', async t => { 312 | const {body} = await got(`${s.url}`, { 313 | responseType: 'json', 314 | hooks: { 315 | afterResponse: [ 316 | response => { 317 | response.body = '{"hello": "world"}'; 318 | 319 | return response; 320 | } 321 | ] 322 | } 323 | }); 324 | t.is(body.hello, 'world'); 325 | }); 326 | 327 | test('afterResponse allows to retry', async t => { 328 | const {statusCode} = await got(`${s.url}/401`, { 329 | hooks: { 330 | afterResponse: [ 331 | (response, retryWithMergedOptions) => { 332 | if (response.statusCode === 401) { 333 | return retryWithMergedOptions({ 334 | headers: { 335 | token: 'unicorn' 336 | } 337 | }); 338 | } 339 | 340 | return response; 341 | } 342 | ] 343 | } 344 | }); 345 | t.is(statusCode, 200); 346 | }); 347 | 348 | test('no infinity loop when retrying on afterResponse', async t => { 349 | await t.throwsAsync(got(`${s.url}/401`, { 350 | retry: 0, 351 | hooks: { 352 | afterResponse: [ 353 | (response, retryWithMergedOptions) => { 354 | return retryWithMergedOptions({ 355 | headers: { 356 | token: 'invalid' 357 | } 358 | }); 359 | } 360 | ] 361 | } 362 | }), {instanceOf: got.HTTPError, message: 'Response code 401 (Unauthorized)'}); 363 | }); 364 | 365 | test.serial('throws on afterResponse retry failure', async t => { 366 | visited401then500 = false; 367 | 368 | await t.throwsAsync(got(`${s.url}/401then500`, { 369 | retry: 1, 370 | hooks: { 371 | afterResponse: [ 372 | (response, retryWithMergedOptions) => { 373 | if (response.statusCode === 401) { 374 | return retryWithMergedOptions({ 375 | headers: { 376 | token: 'unicorn' 377 | } 378 | }); 379 | } 380 | 381 | return response; 382 | } 383 | ] 384 | } 385 | }), {instanceOf: got.HTTPError, message: 'Response code 500 (Internal Server Error)'}); 386 | }); 387 | 388 | test.serial('doesn\'t throw on afterResponse retry HTTP failure if throwHttpErrors is false', async t => { 389 | visited401then500 = false; 390 | 391 | const {statusCode} = await got(`${s.url}/401then500`, { 392 | throwHttpErrors: false, 393 | retry: 1, 394 | hooks: { 395 | afterResponse: [ 396 | (response, retryWithMergedOptions) => { 397 | if (response.statusCode === 401) { 398 | return retryWithMergedOptions({ 399 | headers: { 400 | token: 'unicorn' 401 | } 402 | }); 403 | } 404 | 405 | return response; 406 | } 407 | ] 408 | } 409 | }); 410 | t.is(statusCode, 500); 411 | }); 412 | 413 | test('beforeError is called with an error', async t => { 414 | await t.throwsAsync(() => got(s.url, { 415 | request: () => { 416 | throw error; 417 | }, 418 | hooks: { 419 | beforeError: [error2 => { 420 | t.true(error2 instanceof Error); 421 | return error2; 422 | }] 423 | } 424 | }), errorString); 425 | }); 426 | 427 | test('beforeError allows modifications', async t => { 428 | const errorString2 = 'foobar'; 429 | 430 | await t.throwsAsync(() => got(s.url, { 431 | request: () => { 432 | throw error; 433 | }, 434 | hooks: { 435 | beforeError: [() => { 436 | return new Error(errorString2); 437 | }] 438 | } 439 | }), errorString2); 440 | }); 441 | -------------------------------------------------------------------------------- /source/request-as-event-emitter.ts: -------------------------------------------------------------------------------- 1 | import urlLib, {URL, URLSearchParams} from 'url'; // TODO: Use the `URL` global when targeting Node.js 10 2 | import util from 'util'; 3 | import EventEmitter from 'events'; 4 | import {Transform as TransformStream} from 'stream'; 5 | import http from 'http'; 6 | import https from 'https'; 7 | import CacheableRequest from 'cacheable-request'; 8 | import toReadableStream from 'to-readable-stream'; 9 | import is from '@sindresorhus/is'; 10 | import timer from '@szmarczak/http-timer'; 11 | import timedOut, {TimeoutError as TimedOutTimeoutError} from './utils/timed-out'; 12 | import getBodySize from './utils/get-body-size'; 13 | import isFormData from './utils/is-form-data'; 14 | import getResponse from './get-response'; 15 | import {uploadProgress} from './progress'; 16 | import {CacheError, UnsupportedProtocolError, MaxRedirectsError, RequestError, TimeoutError} from './errors'; 17 | import urlToOptions from './utils/url-to-options'; 18 | 19 | const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]); 20 | const allMethodRedirectCodes = new Set([300, 303, 307, 308]); 21 | 22 | const withoutBody = new Set(['GET', 'HEAD']); 23 | 24 | export default (options, input?: TransformStream) => { 25 | const emitter = new EventEmitter(); 26 | const redirects = []; 27 | let currentRequest; 28 | let requestUrl; 29 | let redirectString; 30 | let uploadBodySize; 31 | let retryCount = 0; 32 | let shouldAbort = false; 33 | 34 | const setCookie = options.cookieJar ? util.promisify(options.cookieJar.setCookie.bind(options.cookieJar)) : null; 35 | const getCookieString = options.cookieJar ? util.promisify(options.cookieJar.getCookieString.bind(options.cookieJar)) : null; 36 | const agents = is.object(options.agent) ? options.agent : null; 37 | 38 | const emitError = async error => { 39 | try { 40 | for (const hook of options.hooks.beforeError) { 41 | // eslint-disable-next-line no-await-in-loop 42 | error = await hook(error); 43 | } 44 | 45 | emitter.emit('error', error); 46 | } catch (error2) { 47 | emitter.emit('error', error2); 48 | } 49 | }; 50 | 51 | const get = async options => { 52 | const currentUrl = redirectString || requestUrl; 53 | 54 | if (options.protocol !== 'http:' && options.protocol !== 'https:') { 55 | throw new UnsupportedProtocolError(options); 56 | } 57 | 58 | decodeURI(currentUrl); 59 | 60 | let fn; 61 | if (is.function_(options.request)) { 62 | fn = {request: options.request}; 63 | } else { 64 | fn = options.protocol === 'https:' ? https : http; 65 | } 66 | 67 | if (agents) { 68 | const protocolName = options.protocol === 'https:' ? 'https' : 'http'; 69 | options.agent = agents[protocolName] || options.agent; 70 | } 71 | 72 | /* istanbul ignore next: electron.net is broken */ 73 | if (options.useElectronNet && (process.versions as any).electron) { 74 | // @ts-ignore 75 | const r = ({x: require})['yx'.slice(1)]; // Trick webpack 76 | const electron = r('electron'); 77 | fn = electron.net || electron.remote.net; 78 | } 79 | 80 | if (options.cookieJar) { 81 | const cookieString = await getCookieString(currentUrl, {}); 82 | 83 | if (is.nonEmptyString(cookieString)) { 84 | options.headers.cookie = cookieString; 85 | } 86 | } 87 | 88 | let timings; 89 | const handleResponse = async response => { 90 | try { 91 | /* istanbul ignore next: fixes https://github.com/electron/electron/blob/cbb460d47628a7a146adf4419ed48550a98b2923/lib/browser/api/net.js#L59-L65 */ 92 | if (options.useElectronNet) { 93 | response = new Proxy(response, { 94 | get: (target, name) => { 95 | if (name === 'trailers' || name === 'rawTrailers') { 96 | return []; 97 | } 98 | 99 | const value = target[name]; 100 | return is.function_(value) ? value.bind(target) : value; 101 | } 102 | }); 103 | } 104 | 105 | const {statusCode} = response; 106 | response.url = currentUrl; 107 | response.requestUrl = requestUrl; 108 | response.retryCount = retryCount; 109 | response.timings = timings; 110 | response.redirectUrls = redirects; 111 | response.request = { 112 | gotOptions: options 113 | }; 114 | 115 | const rawCookies = response.headers['set-cookie']; 116 | if (options.cookieJar && rawCookies) { 117 | await Promise.all(rawCookies.map(rawCookie => setCookie(rawCookie, response.url))); 118 | } 119 | 120 | if (options.followRedirect && 'location' in response.headers) { 121 | if (allMethodRedirectCodes.has(statusCode) || (getMethodRedirectCodes.has(statusCode) && (options.method === 'GET' || options.method === 'HEAD'))) { 122 | response.resume(); // We're being redirected, we don't care about the response. 123 | 124 | if (statusCode === 303) { 125 | // Server responded with "see other", indicating that the resource exists at another location, 126 | // and the client should request it from that location via GET or HEAD. 127 | options.method = 'GET'; 128 | } 129 | 130 | if (redirects.length >= 10) { 131 | throw new MaxRedirectsError(statusCode, redirects, options); 132 | } 133 | 134 | // Handles invalid URLs. See https://github.com/sindresorhus/got/issues/604 135 | const redirectBuffer = Buffer.from(response.headers.location, 'binary').toString(); 136 | const redirectURL = new URL(redirectBuffer, currentUrl); 137 | redirectString = redirectURL.toString(); 138 | 139 | redirects.push(redirectString); 140 | 141 | const redirectOptions = { 142 | ...options, 143 | port: null, 144 | ...urlToOptions(redirectURL) 145 | }; 146 | 147 | for (const hook of options.hooks.beforeRedirect) { 148 | // eslint-disable-next-line no-await-in-loop 149 | await hook(redirectOptions); 150 | } 151 | 152 | emitter.emit('redirect', response, redirectOptions); 153 | 154 | await get(redirectOptions); 155 | return; 156 | } 157 | } 158 | 159 | getResponse(response, options, emitter); 160 | } catch (error) { 161 | emitError(error); 162 | } 163 | }; 164 | 165 | const handleRequest = request => { 166 | if (shouldAbort) { 167 | request.abort(); 168 | return; 169 | } 170 | 171 | currentRequest = request; 172 | 173 | request.on('error', error => { 174 | if (request.aborted || error.message === 'socket hang up') { 175 | return; 176 | } 177 | 178 | if (error instanceof TimedOutTimeoutError) { 179 | error = new TimeoutError(error, timings, options); 180 | } else { 181 | error = new RequestError(error, options); 182 | } 183 | 184 | // TODO: Properly type this 185 | if ((emitter as any).retry(error) === false) { 186 | emitError(error); 187 | } 188 | }); 189 | 190 | timings = timer(request); 191 | 192 | uploadProgress(request, emitter, uploadBodySize); 193 | 194 | if (options.gotTimeout) { 195 | timedOut(request, options.gotTimeout, options); 196 | } 197 | 198 | emitter.emit('request', request); 199 | 200 | const uploadComplete = () => { 201 | request.emit('upload-complete'); 202 | }; 203 | 204 | try { 205 | if (is.nodeStream(options.body)) { 206 | options.body.once('end', uploadComplete); 207 | options.body.pipe(request); 208 | options.body = undefined; 209 | } else if (options.body) { 210 | request.end(options.body, uploadComplete); 211 | } else if (input && (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH')) { 212 | input.once('end', uploadComplete); 213 | input.pipe(request); 214 | } else { 215 | request.end(uploadComplete); 216 | } 217 | } catch (error) { 218 | emitError(new RequestError(error, options)); 219 | } 220 | }; 221 | 222 | if (options.cache) { 223 | const cacheableRequest = new CacheableRequest(fn.request, options.cache); 224 | const cacheRequest = cacheableRequest(options, handleResponse); 225 | 226 | cacheRequest.once('error', error => { 227 | if (error instanceof CacheableRequest.RequestError) { 228 | emitError(new RequestError(error, options)); 229 | } else { 230 | emitError(new CacheError(error, options)); 231 | } 232 | }); 233 | 234 | cacheRequest.once('request', handleRequest); 235 | } else { 236 | // Catches errors thrown by calling fn.request(...) 237 | try { 238 | handleRequest(fn.request(options, handleResponse)); 239 | } catch (error) { 240 | emitError(new RequestError(error, options)); 241 | } 242 | } 243 | }; 244 | 245 | // TODO: Properly type this 246 | (emitter as any).retry = error => { 247 | let backoff; 248 | 249 | try { 250 | backoff = options.retry.retries(++retryCount, error); 251 | } catch (error2) { 252 | emitError(error2); 253 | return; 254 | } 255 | 256 | if (backoff) { 257 | const retry = async options => { 258 | try { 259 | for (const hook of options.hooks.beforeRetry) { 260 | // eslint-disable-next-line no-await-in-loop 261 | await hook(options, error, retryCount); 262 | } 263 | 264 | await get(options); 265 | } catch (error) { 266 | emitError(error); 267 | } 268 | }; 269 | 270 | setTimeout(retry, backoff, {...options, forceRefresh: true}); 271 | return true; 272 | } 273 | 274 | return false; 275 | }; 276 | 277 | // TODO: Properly type this 278 | (emitter as any).abort = () => { 279 | if (currentRequest) { 280 | currentRequest.abort(); 281 | } else { 282 | shouldAbort = true; 283 | } 284 | }; 285 | 286 | setImmediate(async () => { 287 | try { 288 | for (const hook of options.hooks.beforeRequest) { 289 | // eslint-disable-next-line no-await-in-loop 290 | await hook(options); 291 | } 292 | 293 | // Serialize body 294 | const {body, headers} = options; 295 | const isForm = !is.nullOrUndefined(options.form); 296 | const isJSON = !is.nullOrUndefined(options.json); 297 | const isBody = !is.nullOrUndefined(body); 298 | if ((isBody || isForm || isJSON) && withoutBody.has(options.method)) { 299 | throw new TypeError(`The \`${options.method}\` method cannot be used with a body`); 300 | } 301 | 302 | if (isBody) { 303 | if (isForm || isJSON) { 304 | throw new TypeError('The `body` option cannot be used with the `json` option or `form` option'); 305 | } 306 | 307 | if (is.object(body) && isFormData(body)) { 308 | // Special case for https://github.com/form-data/form-data 309 | headers['content-type'] = headers['content-type'] || `multipart/form-data; boundary=${body.getBoundary()}`; 310 | } else if (!is.nodeStream(body) && !is.string(body) && !is.buffer(body)) { 311 | throw new TypeError('The `body` option must be a stream.Readable, string, Buffer, Object or Array'); 312 | } 313 | } else if (isForm) { 314 | if (!is.object(options.form)) { 315 | throw new TypeError('The `form` option must be an Object'); 316 | } 317 | 318 | headers['content-type'] = headers['content-type'] || 'application/x-www-form-urlencoded'; 319 | options.body = (new URLSearchParams(options.form)).toString(); 320 | } else if (isJSON) { 321 | headers['content-type'] = headers['content-type'] || 'application/json'; 322 | options.body = JSON.stringify(options.json); 323 | } 324 | 325 | // Convert buffer to stream to receive upload progress events (#322) 326 | if (is.buffer(body)) { 327 | options.body = toReadableStream(body); 328 | uploadBodySize = body.length; 329 | } else { 330 | uploadBodySize = await getBodySize(options); 331 | } 332 | 333 | if (is.undefined(headers['content-length']) && is.undefined(headers['transfer-encoding'])) { 334 | if ((uploadBodySize > 0 || options.method === 'PUT') && !is.undefined(uploadBodySize)) { 335 | headers['content-length'] = uploadBodySize; 336 | } 337 | } 338 | 339 | if (!options.stream && options.responseType === 'json' && is.undefined(headers.accept)) { 340 | options.headers.accept = 'application/json'; 341 | } 342 | 343 | requestUrl = options.href || (new URL(options.path, urlLib.format(options))).toString(); 344 | 345 | await get(options); 346 | } catch (error) { 347 | emitError(error); 348 | } 349 | }); 350 | 351 | return emitter; 352 | }; 353 | -------------------------------------------------------------------------------- /test/timeout.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import net from 'net'; 3 | import stream from 'stream'; 4 | import getStream from 'get-stream'; 5 | import test from 'ava'; 6 | import pEvent from 'p-event'; 7 | import delay from 'delay'; 8 | import got from '../source'; 9 | import {createServer, createSSLServer} from './helpers/server'; 10 | 11 | let s; 12 | let ss; 13 | 14 | const slowDataStream = () => { 15 | const slowStream = new stream.PassThrough(); 16 | let count = 0; 17 | const interval = setInterval(() => { 18 | if (count++ < 10) { 19 | return slowStream.push('data\n'.repeat(100)); 20 | } 21 | 22 | clearInterval(interval); 23 | slowStream.push(null); 24 | }, 100); 25 | return slowStream; 26 | }; 27 | 28 | const requestDelay = 750; 29 | const requestTimeout = requestDelay - 30; 30 | 31 | const errorMatcher = { 32 | instanceOf: got.TimeoutError, 33 | code: 'ETIMEDOUT' 34 | }; 35 | 36 | const keepAliveAgent = new http.Agent({ 37 | keepAlive: true 38 | }); 39 | 40 | test.before('setup', async () => { 41 | [s, ss] = await Promise.all([createServer(), createSSLServer()]); 42 | 43 | s.on('/', (request, response) => { 44 | request.on('data', () => {}); 45 | request.on('end', async () => { 46 | await delay(requestDelay); 47 | response.end('OK'); 48 | }); 49 | }); 50 | 51 | s.on('/delayed', async (request, response) => { 52 | response.write('O'); 53 | await delay(requestDelay); 54 | response.end('K'); 55 | }); 56 | 57 | s.on('/download', (request, response) => { 58 | response.writeHead(200, { 59 | 'transfer-encoding': 'chunked' 60 | }); 61 | response.flushHeaders(); 62 | slowDataStream().pipe(response); 63 | }); 64 | 65 | s.on('/prime', (request, response) => { 66 | response.end('OK'); 67 | }); 68 | 69 | ss.on('/', (request, response) => { 70 | response.end('OK'); 71 | }); 72 | 73 | await Promise.all([s.listen(s.port), ss.listen(ss.port)]); 74 | }); 75 | 76 | test('timeout option (ETIMEDOUT)', async t => { 77 | await t.throwsAsync( 78 | got(s.url, { 79 | timeout: 0, 80 | retry: 0 81 | }), 82 | { 83 | ...errorMatcher, 84 | message: 'Timeout awaiting \'request\' for 0ms' 85 | } 86 | ); 87 | }); 88 | 89 | test('timeout option as object (ETIMEDOUT)', async t => { 90 | await t.throwsAsync( 91 | got(s.url, { 92 | timeout: {socket: requestDelay * 2.5, request: 1}, 93 | retry: 0 94 | }), 95 | { 96 | ...errorMatcher, 97 | message: 'Timeout awaiting \'request\' for 1ms' 98 | } 99 | ); 100 | }); 101 | 102 | test('socket timeout', async t => { 103 | await t.throwsAsync( 104 | got(s.url, { 105 | timeout: {socket: requestTimeout}, 106 | retry: 0 107 | }), 108 | { 109 | instanceOf: got.TimeoutError, 110 | code: 'ETIMEDOUT', 111 | message: `Timeout awaiting 'socket' for ${requestTimeout}ms` 112 | } 113 | ); 114 | }); 115 | 116 | test('send timeout', async t => { 117 | await t.throwsAsync( 118 | got(s.url, { 119 | timeout: {send: 1}, 120 | retry: 0 121 | }), 122 | { 123 | ...errorMatcher, 124 | message: 'Timeout awaiting \'send\' for 1ms' 125 | } 126 | ); 127 | }); 128 | 129 | test('send timeout (keepalive)', async t => { 130 | await got(`${s.url}/prime`, {agent: keepAliveAgent}); 131 | await t.throwsAsync( 132 | got.post(s.url, { 133 | agent: keepAliveAgent, 134 | timeout: {send: 1}, 135 | retry: 0, 136 | body: slowDataStream() 137 | }).on('request', request => { 138 | request.once('socket', socket => { 139 | t.false(socket.connecting); 140 | socket.once('connect', () => { 141 | t.fail('\'connect\' event fired, invalidating test'); 142 | }); 143 | }); 144 | }), 145 | { 146 | ...errorMatcher, 147 | message: 'Timeout awaiting \'send\' for 1ms' 148 | } 149 | ); 150 | }); 151 | 152 | test('response timeout', async t => { 153 | await t.throwsAsync( 154 | got(s.url, { 155 | timeout: {response: 1}, 156 | retry: 0 157 | }), 158 | { 159 | ...errorMatcher, 160 | message: 'Timeout awaiting \'response\' for 1ms' 161 | } 162 | ); 163 | }); 164 | 165 | test('response timeout unaffected by slow upload', async t => { 166 | await got.post(s.url, { 167 | timeout: {response: requestDelay * 2}, 168 | retry: 0, 169 | body: slowDataStream() 170 | }).on('request', request => { 171 | request.on('error', error => { 172 | t.fail(`unexpected error: ${error}`); 173 | }); 174 | }); 175 | await delay(requestDelay * 3); 176 | t.pass('no error emitted'); 177 | }); 178 | 179 | test('response timeout unaffected by slow download', async t => { 180 | await got(`${s.url}/download`, { 181 | timeout: {response: 100}, 182 | retry: 0 183 | }).on('request', request => { 184 | request.on('error', error => { 185 | t.fail(`unexpected error: ${error}`); 186 | }); 187 | }); 188 | await delay(requestDelay * 3); 189 | t.pass('no error emitted'); 190 | }); 191 | 192 | test('response timeout (keepalive)', async t => { 193 | await got(`${s.url}/prime`, {agent: keepAliveAgent}); 194 | await delay(100); 195 | const request = got(s.url, { 196 | agent: keepAliveAgent, 197 | timeout: {response: 1}, 198 | retry: 0 199 | }).on('request', request => { 200 | request.once('socket', socket => { 201 | t.false(socket.connecting); 202 | socket.once('connect', () => { 203 | t.fail('\'connect\' event fired, invalidating test'); 204 | }); 205 | }); 206 | }); 207 | await t.throwsAsync(request, { 208 | ...errorMatcher, 209 | message: 'Timeout awaiting \'response\' for 1ms' 210 | }); 211 | }); 212 | 213 | test('connect timeout', async t => { 214 | await t.throwsAsync( 215 | got({ 216 | host: s.host, 217 | port: s.port, 218 | createConnection: options => { 219 | const socket = new net.Socket(options); 220 | // @ts-ignore 221 | socket.connecting = true; 222 | setImmediate( 223 | socket.emit.bind(socket), 224 | 'lookup', 225 | null, 226 | '127.0.0.1', 227 | 4, 228 | 'localhost' 229 | ); 230 | return socket; 231 | } 232 | }, { 233 | timeout: {connect: 1}, 234 | retry: 0 235 | }), 236 | { 237 | ...errorMatcher, 238 | message: 'Timeout awaiting \'connect\' for 1ms' 239 | } 240 | ); 241 | }); 242 | 243 | test('connect timeout (ip address)', async t => { 244 | await t.throwsAsync( 245 | got({ 246 | hostname: '127.0.0.1', 247 | port: s.port, 248 | createConnection: options => { 249 | const socket = new net.Socket(options); 250 | // @ts-ignore 251 | socket.connecting = true; 252 | return socket; 253 | } 254 | }, { 255 | timeout: {connect: 1}, 256 | retry: 0 257 | }), 258 | { 259 | ...errorMatcher, 260 | message: 'Timeout awaiting \'connect\' for 1ms' 261 | } 262 | ); 263 | }); 264 | 265 | test('secureConnect timeout', async t => { 266 | await t.throwsAsync( 267 | got(ss.url, { 268 | timeout: {secureConnect: 1}, 269 | retry: 0, 270 | rejectUnauthorized: false 271 | }), 272 | { 273 | ...errorMatcher, 274 | message: 'Timeout awaiting \'secureConnect\' for 1ms' 275 | } 276 | ); 277 | }); 278 | 279 | test('secureConnect timeout not breached', async t => { 280 | const secureConnect = 200; 281 | await got(ss.url, { 282 | timeout: {secureConnect}, 283 | retry: 0, 284 | rejectUnauthorized: false 285 | }).on('request', request => { 286 | request.on('error', error => { 287 | t.fail(`error emitted: ${error}`); 288 | }); 289 | }); 290 | await delay(secureConnect * 2); 291 | t.pass('no error emitted'); 292 | }); 293 | 294 | test('lookup timeout', async t => { 295 | await t.throwsAsync( 296 | got({ 297 | host: s.host, 298 | port: s.port, 299 | lookup: () => { } 300 | }, { 301 | timeout: {lookup: 1}, 302 | retry: 0 303 | }), 304 | { 305 | ...errorMatcher, 306 | message: 'Timeout awaiting \'lookup\' for 1ms' 307 | } 308 | ); 309 | }); 310 | 311 | test('lookup timeout no error (ip address)', async t => { 312 | await got({ 313 | hostname: '127.0.0.1', 314 | port: s.port, 315 | protocol: 'http:' 316 | }, { 317 | timeout: {lookup: 100}, 318 | retry: 0 319 | }).on('request', request => { 320 | request.on('error', error => { 321 | t.fail(`error emitted: ${error}`); 322 | }); 323 | }); 324 | await delay(100); 325 | t.pass('no error emitted'); 326 | }); 327 | 328 | test('lookup timeout no error (keepalive)', async t => { 329 | await got(`${s.url}/prime`, {agent: keepAliveAgent}); 330 | await got(s.url, { 331 | agent: keepAliveAgent, 332 | timeout: {lookup: 100}, 333 | retry: 0 334 | }).on('request', request => { 335 | request.once('connect', () => { 336 | t.fail('connect event fired, invalidating test'); 337 | }); 338 | request.on('error', error => { 339 | t.fail(`error emitted: ${error}`); 340 | }); 341 | }); 342 | await delay(100); 343 | t.pass('no error emitted'); 344 | }); 345 | 346 | test('request timeout', async t => { 347 | await t.throwsAsync( 348 | got(s.url, { 349 | timeout: {request: requestTimeout}, 350 | retry: 0 351 | }), 352 | { 353 | ...errorMatcher, 354 | message: `Timeout awaiting 'request' for ${requestTimeout}ms` 355 | } 356 | ); 357 | }); 358 | 359 | test('retries on timeout (ESOCKETTIMEDOUT)', async t => { 360 | let tried = false; 361 | 362 | await t.throwsAsync(got(s.url, { 363 | timeout: requestTimeout, 364 | retry: { 365 | retries: () => { 366 | if (tried) { 367 | return 0; 368 | } 369 | 370 | tried = true; 371 | return 1; 372 | } 373 | } 374 | }), { 375 | ...errorMatcher, 376 | message: `Timeout awaiting 'request' for ${requestTimeout}ms` 377 | }); 378 | 379 | t.true(tried); 380 | }); 381 | 382 | test('retries on timeout (ETIMEDOUT)', async t => { 383 | let tried = false; 384 | 385 | await t.throwsAsync(got(s.url, { 386 | timeout: 0, 387 | retry: { 388 | retries: () => { 389 | if (tried) { 390 | return 0; 391 | } 392 | 393 | tried = true; 394 | return 1; 395 | } 396 | } 397 | }), {...errorMatcher}); 398 | 399 | t.true(tried); 400 | }); 401 | 402 | test('timeout with streams', async t => { 403 | const stream = got.stream(s.url, { 404 | timeout: 0, 405 | retry: 0 406 | }); 407 | await t.throwsAsync(pEvent(stream, 'response'), {code: 'ETIMEDOUT'}); 408 | }); 409 | 410 | test('no error emitted when timeout is not breached (stream)', async t => { 411 | const stream = got.stream(s.url, { 412 | retry: 0, 413 | timeout: { 414 | request: requestDelay * 2 415 | } 416 | }); 417 | stream.on('error', err => { 418 | t.fail(`error was emitted: ${err}`); 419 | }); 420 | await getStream(stream); 421 | await delay(requestDelay * 3); 422 | t.pass(); 423 | }); 424 | 425 | test('no error emitted when timeout is not breached (promise)', async t => { 426 | await got(s.url, { 427 | retry: 0, 428 | timeout: { 429 | request: requestDelay * 2 430 | } 431 | }).on('request', request => { 432 | // 'error' events are not emitted by the Promise interface, so attach 433 | // directly to the request object 434 | request.on('error', error => { 435 | t.fail(`error was emitted: ${error}`); 436 | }); 437 | }); 438 | await delay(requestDelay * 3); 439 | t.pass(); 440 | }); 441 | 442 | test('no unhandled `socket hung up` errors', async t => { 443 | await t.throwsAsync(got(s.url, {retry: 0, timeout: requestDelay / 2}), {instanceOf: got.TimeoutError}); 444 | await delay(requestDelay); 445 | }); 446 | 447 | test('no more timeouts after an error', async t => { 448 | await t.throwsAsync(got(`http://${Date.now()}.dev`, { 449 | retry: 1, 450 | timeout: { 451 | lookup: 1, 452 | connect: 1, 453 | secureConnect: 1, 454 | socket: 1, 455 | response: 1, 456 | send: 1, 457 | request: 1 458 | } 459 | }), {instanceOf: got.GotError}); // Don't check the message, because it may throw ENOTFOUND before the timeout. 460 | 461 | // Wait a bit more to check if there are any unhandled errors 462 | await delay(10); 463 | }); 464 | 465 | test('socket timeout is canceled on error', async t => { 466 | const message = 'oh, snap!'; 467 | 468 | const promise = got(s.url, { 469 | timeout: {socket: requestTimeout}, 470 | retry: 0 471 | }).on('request', request => { 472 | request.emit('error', new Error(message)); 473 | }); 474 | 475 | await t.throwsAsync(promise, {message}); 476 | // Wait a bit more to check if there are any unhandled errors 477 | await delay(10); 478 | }); 479 | 480 | test('no memory leak when using socket timeout and keepalive agent', async t => { 481 | const promise = got(s.url, { 482 | agent: keepAliveAgent, 483 | timeout: {socket: requestDelay * 2} 484 | }); 485 | 486 | let socket; 487 | promise.on('request', request => { 488 | request.on('socket', () => { 489 | socket = request.socket; 490 | }); 491 | }); 492 | 493 | await promise; 494 | 495 | t.is(socket.listenerCount('timeout'), 0); 496 | }); 497 | --------------------------------------------------------------------------------