got 9.2.1 → 9.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,38 +6,42 @@ const http = require('http');
6
6
  const https = require('https');
7
7
  const urlLib = require('url');
8
8
  const CacheableRequest = require('cacheable-request');
9
+ const toReadableStream = require('to-readable-stream');
9
10
  const is = require('@sindresorhus/is');
10
11
  const timer = require('@szmarczak/http-timer');
11
- const timedOut = require('./timed-out');
12
- const getBodySize = require('./get-body-size');
12
+ const timedOut = require('./utils/timed-out');
13
+ const getBodySize = require('./utils/get-body-size');
13
14
  const getResponse = require('./get-response');
14
15
  const progress = require('./progress');
15
- const {GotError, CacheError, UnsupportedProtocolError, MaxRedirectsError, RequestError} = require('./errors');
16
+ const {CacheError, UnsupportedProtocolError, MaxRedirectsError, RequestError, TimeoutError} = require('./errors');
17
+ const urlToOptions = require('./utils/url-to-options');
16
18
 
17
19
  const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]);
18
20
  const allMethodRedirectCodes = new Set([300, 303, 307, 308]);
19
21
 
20
- module.exports = options => {
22
+ module.exports = (options, input) => {
21
23
  const emitter = new EventEmitter();
22
- const requestUrl = options.href || (new URL(options.path, urlLib.format(options))).toString();
23
24
  const redirects = [];
24
- const agents = is.object(options.agent) ? options.agent : null;
25
- let retryCount = 0;
26
- let retryTries = 0;
27
- let redirectUrl;
25
+ let currentRequest;
26
+ let requestUrl;
27
+ let redirectString;
28
28
  let uploadBodySize;
29
+ let retryCount = 0;
30
+ let shouldAbort = false;
29
31
 
30
32
  const setCookie = options.cookieJar ? util.promisify(options.cookieJar.setCookie.bind(options.cookieJar)) : null;
31
33
  const getCookieString = options.cookieJar ? util.promisify(options.cookieJar.getCookieString.bind(options.cookieJar)) : null;
34
+ const agents = is.object(options.agent) ? options.agent : null;
32
35
 
33
36
  const get = async options => {
34
- const currentUrl = redirectUrl || requestUrl;
37
+ const currentUrl = redirectString || requestUrl;
35
38
 
36
39
  if (options.protocol !== 'http:' && options.protocol !== 'https:') {
37
- emitter.emit('error', new UnsupportedProtocolError(options));
38
- return;
40
+ throw new UnsupportedProtocolError(options);
39
41
  }
40
42
 
43
+ decodeURI(currentUrl);
44
+
41
45
  let fn;
42
46
  if (is.function(options.request)) {
43
47
  fn = {request: options.request};
@@ -58,124 +62,109 @@ module.exports = options => {
58
62
  }
59
63
 
60
64
  if (options.cookieJar) {
61
- try {
62
- const cookieString = await getCookieString(currentUrl, {});
65
+ const cookieString = await getCookieString(currentUrl, {});
63
66
 
64
- if (!is.empty(cookieString)) {
65
- options.headers.cookie = cookieString;
66
- }
67
- } catch (error) {
68
- emitter.emit('error', error);
67
+ if (is.nonEmptyString(cookieString)) {
68
+ options.headers.cookie = cookieString;
69
69
  }
70
70
  }
71
71
 
72
72
  let timings;
73
- const cacheableRequest = new CacheableRequest(fn.request, options.cache);
74
- const cacheReq = cacheableRequest(options, async response => {
75
- /* istanbul ignore next: fixes https://github.com/electron/electron/blob/cbb460d47628a7a146adf4419ed48550a98b2923/lib/browser/api/net.js#L59-L65 */
76
- if (options.useElectronNet) {
77
- response = new Proxy(response, {
78
- get: (target, name) => {
79
- if (name === 'trailers' || name === 'rawTrailers') {
80
- return [];
73
+ const handleResponse = async response => {
74
+ try {
75
+ /* istanbul ignore next: fixes https://github.com/electron/electron/blob/cbb460d47628a7a146adf4419ed48550a98b2923/lib/browser/api/net.js#L59-L65 */
76
+ if (options.useElectronNet) {
77
+ response = new Proxy(response, {
78
+ get: (target, name) => {
79
+ if (name === 'trailers' || name === 'rawTrailers') {
80
+ return [];
81
+ }
82
+
83
+ const value = target[name];
84
+ return is.function(value) ? value.bind(target) : value;
81
85
  }
86
+ });
87
+ }
82
88
 
83
- const value = target[name];
84
- return is.function(value) ? value.bind(target) : value;
85
- }
86
- });
87
- }
88
-
89
- const {statusCode} = response;
90
- response.url = currentUrl;
91
- response.requestUrl = requestUrl;
92
- response.retryCount = retryCount;
93
- response.timings = timings;
89
+ const {statusCode} = response;
90
+ response.url = currentUrl;
91
+ response.requestUrl = requestUrl;
92
+ response.retryCount = retryCount;
93
+ response.timings = timings;
94
+ response.redirectUrls = redirects;
94
95
 
95
- const rawCookies = response.headers['set-cookie'];
96
- if (options.cookieJar && rawCookies) {
97
- try {
96
+ const rawCookies = response.headers['set-cookie'];
97
+ if (options.cookieJar && rawCookies) {
98
98
  await Promise.all(rawCookies.map(rawCookie => setCookie(rawCookie, response.url)));
99
- } catch (error) {
100
- emitter.emit('error', error);
101
99
  }
102
- }
103
-
104
- const followRedirect = options.followRedirect && 'location' in response.headers;
105
- const redirectGet = followRedirect && getMethodRedirectCodes.has(statusCode);
106
- const redirectAll = followRedirect && allMethodRedirectCodes.has(statusCode);
107
100
 
108
- if (redirectAll || (redirectGet && (options.method === 'GET' || options.method === 'HEAD'))) {
109
- response.resume();
101
+ if (options.followRedirect && 'location' in response.headers) {
102
+ if (allMethodRedirectCodes.has(statusCode) || (getMethodRedirectCodes.has(statusCode) && (options.method === 'GET' || options.method === 'HEAD'))) {
103
+ response.resume(); // We're being redirected, we don't care about the response.
110
104
 
111
- if (statusCode === 303) {
112
- // Server responded with "see other", indicating that the resource exists at another location,
113
- // and the client should request it from that location via GET or HEAD.
114
- options.method = 'GET';
115
- }
105
+ if (statusCode === 303) {
106
+ // Server responded with "see other", indicating that the resource exists at another location,
107
+ // and the client should request it from that location via GET or HEAD.
108
+ options.method = 'GET';
109
+ }
116
110
 
117
- if (redirects.length >= 10) {
118
- emitter.emit('error', new MaxRedirectsError(statusCode, redirects, options), null, response);
119
- return;
120
- }
111
+ if (redirects.length >= 10) {
112
+ throw new MaxRedirectsError(statusCode, redirects, options);
113
+ }
121
114
 
122
- const bufferString = Buffer.from(response.headers.location, 'binary').toString();
123
- redirectUrl = (new URL(bufferString, urlLib.format(options))).toString();
115
+ // Handles invalid URLs. See https://github.com/sindresorhus/got/issues/604
116
+ const redirectBuffer = Buffer.from(response.headers.location, 'binary').toString();
117
+ const redirectURL = new URL(redirectBuffer, currentUrl);
118
+ redirectString = redirectURL.toString();
124
119
 
125
- try {
126
- decodeURI(redirectUrl);
127
- } catch (error) {
128
- emitter.emit('error', error);
129
- return;
130
- }
120
+ redirects.push(redirectString);
131
121
 
132
- redirects.push(redirectUrl);
122
+ const redirectOpts = {
123
+ ...options,
124
+ ...urlToOptions(redirectURL)
125
+ };
133
126
 
134
- const redirectOpts = {
135
- ...options,
136
- ...urlLib.parse(redirectUrl)
137
- };
127
+ for (const hook of options.hooks.beforeRedirect) {
128
+ // eslint-disable-next-line no-await-in-loop
129
+ await hook(redirectOpts);
130
+ }
138
131
 
139
- emitter.emit('redirect', response, redirectOpts);
132
+ emitter.emit('redirect', response, redirectOpts);
140
133
 
141
- await get(redirectOpts);
142
- return;
143
- }
134
+ await get(redirectOpts);
135
+ return;
136
+ }
137
+ }
144
138
 
145
- try {
146
- getResponse(response, options, emitter, redirects);
139
+ getResponse(response, options, emitter);
147
140
  } catch (error) {
148
141
  emitter.emit('error', error);
149
142
  }
150
- });
143
+ };
151
144
 
152
- cacheReq.on('error', error => {
153
- if (error instanceof CacheableRequest.RequestError) {
154
- emitter.emit('error', new RequestError(error, options));
155
- } else {
156
- emitter.emit('error', new CacheError(error, options));
145
+ const handleRequest = request => {
146
+ if (shouldAbort) {
147
+ request.once('error', () => {});
148
+ request.abort();
149
+ return;
157
150
  }
158
- });
159
151
 
160
- cacheReq.once('request', request => {
161
- let aborted = false;
162
- request.once('abort', _ => {
163
- aborted = true;
164
- });
152
+ currentRequest = request;
165
153
 
166
154
  request.once('error', error => {
167
- if (aborted) {
155
+ if (request.aborted) {
168
156
  return;
169
157
  }
170
158
 
171
- if (!(error instanceof GotError)) {
159
+ if (error instanceof timedOut.TimeoutError) {
160
+ error = new TimeoutError(error, options);
161
+ } else {
172
162
  error = new RequestError(error, options);
173
163
  }
174
- emitter.emit('retry', error, retried => {
175
- if (!retried) {
176
- emitter.emit('error', error);
177
- }
178
- });
164
+
165
+ if (emitter.retry(error) === false) {
166
+ emitter.emit('error', error);
167
+ }
179
168
  });
180
169
 
181
170
  timings = timer(request);
@@ -183,38 +172,109 @@ module.exports = options => {
183
172
  progress.upload(request, emitter, uploadBodySize);
184
173
 
185
174
  if (options.gotTimeout) {
186
- timedOut(request, options);
175
+ timedOut(request, options.gotTimeout, options);
187
176
  }
188
177
 
189
178
  emitter.emit('request', request);
190
- });
179
+
180
+ const uploadComplete = () => {
181
+ request.emit('upload-complete');
182
+ };
183
+
184
+ try {
185
+ if (is.nodeStream(options.body)) {
186
+ options.body.once('end', uploadComplete);
187
+ options.body.pipe(request);
188
+ options.body = undefined;
189
+ } else if (options.body) {
190
+ request.end(options.body, uploadComplete);
191
+ } else if (input && (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH')) {
192
+ input.once('end', uploadComplete);
193
+ input.pipe(request);
194
+ } else {
195
+ request.end(uploadComplete);
196
+ }
197
+ } catch (error) {
198
+ emitter.emit('error', new RequestError(error, options));
199
+ }
200
+ };
201
+
202
+ if (options.cache) {
203
+ const cacheableRequest = new CacheableRequest(fn.request, options.cache);
204
+ const cacheReq = cacheableRequest(options, handleResponse);
205
+
206
+ cacheReq.once('error', error => {
207
+ if (error instanceof CacheableRequest.RequestError) {
208
+ emitter.emit('error', new RequestError(error, options));
209
+ } else {
210
+ emitter.emit('error', new CacheError(error, options));
211
+ }
212
+ });
213
+
214
+ cacheReq.once('request', handleRequest);
215
+ } else {
216
+ // Catches errors thrown by calling fn.request(...)
217
+ try {
218
+ handleRequest(fn.request(options, handleResponse));
219
+ } catch (error) {
220
+ emitter.emit('error', new RequestError(error, options));
221
+ }
222
+ }
191
223
  };
192
224
 
193
- emitter.on('retry', (error, cb) => {
225
+ emitter.retry = error => {
194
226
  let backoff;
227
+
195
228
  try {
196
- backoff = options.gotRetry.retries(++retryTries, error);
229
+ backoff = options.retry.retries(++retryCount, error);
197
230
  } catch (error2) {
198
231
  emitter.emit('error', error2);
199
232
  return;
200
233
  }
201
234
 
202
235
  if (backoff) {
203
- retryCount++;
204
- setTimeout(get, backoff, {...options, forceRefresh: true});
205
- cb(true);
206
- return;
236
+ const retry = async options => {
237
+ try {
238
+ for (const hook of options.hooks.beforeRetry) {
239
+ // eslint-disable-next-line no-await-in-loop
240
+ await hook(options, error, retryCount);
241
+ }
242
+
243
+ await get(options);
244
+ } catch (error) {
245
+ emitter.emit('error', error);
246
+ }
247
+ };
248
+
249
+ setTimeout(retry, backoff, {...options, forceRefresh: true});
250
+ return true;
207
251
  }
208
252
 
209
- cb(false);
210
- });
253
+ return false;
254
+ };
255
+
256
+ emitter.abort = () => {
257
+ if (currentRequest) {
258
+ currentRequest.once('error', () => {});
259
+ currentRequest.abort();
260
+ } else {
261
+ shouldAbort = true;
262
+ }
263
+ };
211
264
 
212
265
  setImmediate(async () => {
213
266
  try {
214
- uploadBodySize = await getBodySize(options);
267
+ // Convert buffer to stream to receive upload progress events (#322)
268
+ const {body} = options;
269
+ if (is.buffer(body)) {
270
+ options.body = toReadableStream(body);
271
+ uploadBodySize = body.length;
272
+ } else {
273
+ uploadBodySize = await getBodySize(options);
274
+ }
215
275
 
216
276
  if (is.undefined(options.headers['content-length']) && is.undefined(options.headers['transfer-encoding'])) {
217
- if (uploadBodySize > 0 || options.method === 'PUT') {
277
+ if ((uploadBodySize > 0 || options.method === 'PUT') && !is.null(uploadBodySize)) {
218
278
  options.headers['content-length'] = uploadBodySize;
219
279
  }
220
280
  }
@@ -224,6 +284,8 @@ module.exports = options => {
224
284
  await hook(options);
225
285
  }
226
286
 
287
+ requestUrl = options.href || (new URL(options.path, urlLib.format(options))).toString();
288
+
227
289
  await get(options);
228
290
  } catch (error) {
229
291
  emitter.emit('error', error);
File without changes
@@ -28,9 +28,5 @@ module.exports = async options => {
28
28
  return size;
29
29
  }
30
30
 
31
- if (is.nodeStream(body) && is.buffer(body._buffer)) {
32
- return body._buffer.length;
33
- }
34
-
35
31
  return null;
36
32
  };
@@ -5,7 +5,10 @@ const WHITELIST = new Set([
5
5
  'ECONNRESET',
6
6
  'EADDRINUSE',
7
7
  'ECONNREFUSED',
8
- 'EPIPE'
8
+ 'EPIPE',
9
+ 'ENOTFOUND',
10
+ 'ENETUNREACH',
11
+ 'EAI_AGAIN'
9
12
  ]);
10
13
 
11
14
  module.exports = error => {
@@ -0,0 +1,153 @@
1
+ 'use strict';
2
+ const net = require('net');
3
+
4
+ class TimeoutError extends Error {
5
+ constructor(threshold, event) {
6
+ super(`Timeout awaiting '${event}' for ${threshold}ms`);
7
+ this.name = 'TimeoutError';
8
+ this.code = 'ETIMEDOUT';
9
+ this.event = event;
10
+ }
11
+ }
12
+
13
+ const reentry = Symbol('reentry');
14
+
15
+ const noop = () => {};
16
+
17
+ module.exports = (request, delays, options) => {
18
+ /* istanbul ignore next: this makes sure timed-out isn't called twice */
19
+ if (request[reentry]) {
20
+ return;
21
+ }
22
+
23
+ request[reentry] = true;
24
+
25
+ let stopNewTimeouts = false;
26
+
27
+ const addTimeout = (delay, callback, ...args) => {
28
+ // An error had been thrown before. Going further would result in uncaught errors.
29
+ // See https://github.com/sindresorhus/got/issues/631#issuecomment-435675051
30
+ if (stopNewTimeouts) {
31
+ return noop;
32
+ }
33
+
34
+ // Event loop order is timers, poll, immediates.
35
+ // The timed event may emit during the current tick poll phase, so
36
+ // defer calling the handler until the poll phase completes.
37
+ let immediate;
38
+ const timeout = setTimeout(() => {
39
+ immediate = setImmediate(callback, delay, ...args);
40
+ /* istanbul ignore next: added in node v9.7.0 */
41
+ if (immediate.unref) {
42
+ immediate.unref();
43
+ }
44
+ }, delay);
45
+
46
+ /* istanbul ignore next: in order to support electron renderer */
47
+ if (timeout.unref) {
48
+ timeout.unref();
49
+ }
50
+
51
+ const cancel = () => {
52
+ clearTimeout(timeout);
53
+ clearImmediate(immediate);
54
+ };
55
+
56
+ cancelers.push(cancel);
57
+
58
+ return cancel;
59
+ };
60
+
61
+ const {host, hostname} = options;
62
+ const timeoutHandler = (delay, event) => {
63
+ request.emit('error', new TimeoutError(delay, event));
64
+ request.once('error', () => {}); // Ignore the `socket hung up` error made by request.abort()
65
+
66
+ request.abort();
67
+ };
68
+
69
+ const cancelers = [];
70
+ const cancelTimeouts = () => {
71
+ stopNewTimeouts = true;
72
+ cancelers.forEach(cancelTimeout => cancelTimeout());
73
+ };
74
+
75
+ request.once('error', cancelTimeouts);
76
+ request.once('response', response => {
77
+ response.once('end', cancelTimeouts);
78
+ });
79
+
80
+ if (delays.request !== undefined) {
81
+ addTimeout(delays.request, timeoutHandler, 'request');
82
+ }
83
+
84
+ if (delays.socket !== undefined) {
85
+ request.setTimeout(delays.socket, () => {
86
+ timeoutHandler(delays.socket, 'socket');
87
+ });
88
+ }
89
+
90
+ if (delays.lookup !== undefined && !request.socketPath && !net.isIP(hostname || host)) {
91
+ request.once('socket', socket => {
92
+ /* istanbul ignore next: hard to test */
93
+ if (socket.connecting) {
94
+ const cancelTimeout = addTimeout(delays.lookup, timeoutHandler, 'lookup');
95
+ socket.once('lookup', cancelTimeout);
96
+ }
97
+ });
98
+ }
99
+
100
+ if (delays.connect !== undefined) {
101
+ request.once('socket', socket => {
102
+ /* istanbul ignore next: hard to test */
103
+ if (socket.connecting) {
104
+ const timeConnect = () => addTimeout(delays.connect, timeoutHandler, 'connect');
105
+
106
+ if (request.socketPath || net.isIP(hostname || host)) {
107
+ socket.once('connect', timeConnect());
108
+ } else {
109
+ socket.once('lookup', error => {
110
+ if (error === null) {
111
+ socket.once('connect', timeConnect());
112
+ }
113
+ });
114
+ }
115
+ }
116
+ });
117
+ }
118
+
119
+ if (delays.secureConnect !== undefined && options.protocol === 'https:') {
120
+ request.once('socket', socket => {
121
+ /* istanbul ignore next: hard to test */
122
+ if (socket.connecting) {
123
+ socket.once('connect', () => {
124
+ const cancelTimeout = addTimeout(delays.secureConnect, timeoutHandler, 'secureConnect');
125
+ socket.once('secureConnect', cancelTimeout);
126
+ });
127
+ }
128
+ });
129
+ }
130
+
131
+ if (delays.send !== undefined) {
132
+ request.once('socket', socket => {
133
+ const timeRequest = () => addTimeout(delays.send, timeoutHandler, 'send');
134
+ /* istanbul ignore next: hard to test */
135
+ if (socket.connecting) {
136
+ socket.once('connect', () => {
137
+ request.once('upload-complete', timeRequest());
138
+ });
139
+ } else {
140
+ request.once('upload-complete', timeRequest());
141
+ }
142
+ });
143
+ }
144
+
145
+ if (delays.response !== undefined) {
146
+ request.once('upload-complete', () => {
147
+ const cancelTimeout = addTimeout(delays.response, timeoutHandler, 'response');
148
+ request.once('response', cancelTimeout);
149
+ });
150
+ }
151
+ };
152
+
153
+ module.exports.TimeoutError = TimeoutError;
@@ -1,36 +0,0 @@
1
- 'use strict';
2
- const merge = require('./merge');
3
- const knownHookEvents = require('./known-hook-events');
4
-
5
- module.exports = (instances, methods) => {
6
- const handlers = instances.map(instance => instance.defaults.handler);
7
- const size = instances.length - 1;
8
-
9
- let options = {};
10
- const hooks = {};
11
- for (const instance of instances) {
12
- options = merge({}, options, instance.defaults.options);
13
-
14
- const instanceHooks = instance.defaults.options.hooks;
15
- for (const name of knownHookEvents) {
16
- if (hooks[name]) {
17
- hooks[name] = hooks[name].concat(instanceHooks[name]);
18
- } else {
19
- hooks[name] = [...instanceHooks[name]];
20
- }
21
- }
22
- }
23
-
24
- options.hooks = hooks;
25
-
26
- return {
27
- methods,
28
- options,
29
- handler: (options, next) => {
30
- let iteration = -1;
31
- const iterate = options => handlers[++iteration](options, iteration === size ? next : iterate);
32
-
33
- return iterate(options);
34
- }
35
- };
36
- };