UNPKG

15.8 kBJavaScriptView Raw
1'use strict';
2const EventEmitter = require('events');
3const http = require('http');
4const https = require('https');
5const PassThrough = require('stream').PassThrough;
6const Transform = require('stream').Transform;
7const urlLib = require('url');
8const fs = require('fs');
9const querystring = require('querystring');
10const CacheableRequest = require('cacheable-request');
11const duplexer3 = require('duplexer3');
12const intoStream = require('into-stream');
13const is = require('@sindresorhus/is');
14const getStream = require('get-stream');
15const timedOut = require('timed-out');
16const urlParseLax = require('url-parse-lax');
17const urlToOptions = require('url-to-options');
18const lowercaseKeys = require('lowercase-keys');
19const decompressResponse = require('decompress-response');
20const mimicResponse = require('mimic-response');
21const isRetryAllowed = require('is-retry-allowed');
22const isURL = require('isurl');
23const PCancelable = require('p-cancelable');
24const pTimeout = require('p-timeout');
25const pify = require('pify');
26const Buffer = require('safe-buffer').Buffer;
27const pkg = require('./package.json');
28const errors = require('./errors');
29
30const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]);
31const allMethodRedirectCodes = new Set([300, 303, 307, 308]);
32
33const isFormData = body => is.nodeStream(body) && is.function(body.getBoundary);
34
35const getBodySize = opts => {
36 const body = opts.body;
37
38 if (opts.headers['content-length']) {
39 return Number(opts.headers['content-length']);
40 }
41
42 if (!body && !opts.stream) {
43 return 0;
44 }
45
46 if (is.string(body)) {
47 return Buffer.byteLength(body);
48 }
49
50 if (isFormData(body)) {
51 return pify(body.getLength.bind(body))();
52 }
53
54 if (body instanceof fs.ReadStream) {
55 return pify(fs.stat)(body.path).then(stat => stat.size);
56 }
57
58 if (is.nodeStream(body) && is.buffer(body._buffer)) {
59 return body._buffer.length;
60 }
61
62 return null;
63};
64
65function requestAsEventEmitter(opts) {
66 opts = opts || {};
67
68 const ee = new EventEmitter();
69 const requestUrl = opts.href || urlLib.resolve(urlLib.format(opts), opts.path);
70 const redirects = [];
71 const agents = is.object(opts.agent) ? opts.agent : null;
72 let retryCount = 0;
73 let redirectUrl;
74 let uploadBodySize;
75 let uploaded = 0;
76
77 const get = opts => {
78 if (opts.protocol !== 'http:' && opts.protocol !== 'https:') {
79 ee.emit('error', new got.UnsupportedProtocolError(opts));
80 return;
81 }
82
83 let fn = opts.protocol === 'https:' ? https : http;
84
85 if (agents) {
86 const protocolName = opts.protocol === 'https:' ? 'https' : 'http';
87 opts.agent = agents[protocolName] || opts.agent;
88 }
89
90 if (opts.useElectronNet && process.versions.electron) {
91 const electron = require('electron');
92 fn = electron.net || electron.remote.net;
93 }
94
95 let progressInterval;
96
97 const cacheableRequest = new CacheableRequest(fn.request, opts.cache);
98 const cacheReq = cacheableRequest(opts, res => {
99 clearInterval(progressInterval);
100
101 ee.emit('uploadProgress', {
102 percent: 1,
103 transferred: uploaded,
104 total: uploadBodySize
105 });
106
107 const statusCode = res.statusCode;
108
109 res.url = redirectUrl || requestUrl;
110 res.requestUrl = requestUrl;
111
112 const followRedirect = opts.followRedirect && 'location' in res.headers;
113 const redirectGet = followRedirect && getMethodRedirectCodes.has(statusCode);
114 const redirectAll = followRedirect && allMethodRedirectCodes.has(statusCode);
115
116 if (redirectAll || (redirectGet && (opts.method === 'GET' || opts.method === 'HEAD'))) {
117 res.resume();
118
119 if (statusCode === 303) {
120 // Server responded with "see other", indicating that the resource exists at another location,
121 // and the client should request it from that location via GET or HEAD.
122 opts.method = 'GET';
123 }
124
125 if (redirects.length >= 10) {
126 ee.emit('error', new got.MaxRedirectsError(statusCode, redirects, opts), null, res);
127 return;
128 }
129
130 const bufferString = Buffer.from(res.headers.location, 'binary').toString();
131
132 redirectUrl = urlLib.resolve(urlLib.format(opts), bufferString);
133
134 redirects.push(redirectUrl);
135
136 const redirectOpts = Object.assign({}, opts, urlLib.parse(redirectUrl));
137
138 ee.emit('redirect', res, redirectOpts);
139
140 get(redirectOpts);
141
142 return;
143 }
144
145 const downloadBodySize = Number(res.headers['content-length']) || null;
146 let downloaded = 0;
147
148 setImmediate(() => {
149 const progressStream = new Transform({
150 transform(chunk, encoding, callback) {
151 downloaded += chunk.length;
152
153 const percent = downloadBodySize ? downloaded / downloadBodySize : 0;
154
155 // Let flush() be responsible for emitting the last event
156 if (percent < 1) {
157 ee.emit('downloadProgress', {
158 percent,
159 transferred: downloaded,
160 total: downloadBodySize
161 });
162 }
163
164 callback(null, chunk);
165 },
166
167 flush(callback) {
168 ee.emit('downloadProgress', {
169 percent: 1,
170 transferred: downloaded,
171 total: downloadBodySize
172 });
173
174 callback();
175 }
176 });
177
178 mimicResponse(res, progressStream);
179 progressStream.redirectUrls = redirects;
180
181 const response = opts.decompress === true &&
182 is.function(decompressResponse) &&
183 opts.method !== 'HEAD' ? decompressResponse(progressStream) : progressStream;
184
185 if (!opts.decompress && ['gzip', 'deflate'].indexOf(res.headers['content-encoding']) !== -1) {
186 opts.encoding = null;
187 }
188
189 ee.emit('response', response);
190
191 ee.emit('downloadProgress', {
192 percent: 0,
193 transferred: 0,
194 total: downloadBodySize
195 });
196
197 res.pipe(progressStream);
198 });
199 });
200
201 cacheReq.on('error', err => {
202 if (err instanceof CacheableRequest.RequestError) {
203 ee.emit('error', new got.RequestError(err, opts));
204 } else {
205 ee.emit('error', new got.CacheError(err, opts));
206 }
207 });
208
209 cacheReq.once('request', req => {
210 req.once('error', err => {
211 clearInterval(progressInterval);
212
213 const backoff = opts.retries(++retryCount, err);
214
215 if (backoff) {
216 setTimeout(get, backoff, opts);
217 return;
218 }
219
220 ee.emit('error', new got.RequestError(err, opts));
221 });
222
223 ee.once('request', req => {
224 ee.emit('uploadProgress', {
225 percent: 0,
226 transferred: 0,
227 total: uploadBodySize
228 });
229
230 if (req.connection) {
231 req.connection.once('connect', () => {
232 const uploadEventFrequency = 150;
233
234 progressInterval = setInterval(() => {
235 const lastUploaded = uploaded;
236 const headersSize = Buffer.byteLength(req._header);
237 uploaded = req.connection.bytesWritten - headersSize;
238
239 // Prevent the known issue of `bytesWritten` being larger than body size
240 if (uploadBodySize && uploaded > uploadBodySize) {
241 uploaded = uploadBodySize;
242 }
243
244 // Don't emit events with unchanged progress and
245 // prevent last event from being emitted, because
246 // it's emitted when `response` is emitted
247 if (uploaded === lastUploaded || uploaded === uploadBodySize) {
248 return;
249 }
250
251 ee.emit('uploadProgress', {
252 percent: uploadBodySize ? uploaded / uploadBodySize : 0,
253 transferred: uploaded,
254 total: uploadBodySize
255 });
256 }, uploadEventFrequency);
257 });
258 }
259 });
260
261 if (opts.gotTimeout) {
262 clearInterval(progressInterval);
263 timedOut(req, opts.gotTimeout);
264 }
265
266 setImmediate(() => {
267 ee.emit('request', req);
268 });
269 });
270 };
271
272 setImmediate(() => {
273 Promise.resolve(getBodySize(opts))
274 .then(size => {
275 uploadBodySize = size;
276 get(opts);
277 })
278 .catch(err => {
279 ee.emit('error', err);
280 });
281 });
282
283 return ee;
284}
285
286function asPromise(opts) {
287 const timeoutFn = requestPromise => opts.gotTimeout && opts.gotTimeout.request ?
288 pTimeout(requestPromise, opts.gotTimeout.request, new got.RequestError({message: 'Request timed out', code: 'ETIMEDOUT'}, opts)) :
289 requestPromise;
290
291 const proxy = new EventEmitter();
292
293 const cancelable = new PCancelable((onCancel, resolve, reject) => {
294 const ee = requestAsEventEmitter(opts);
295 let cancelOnRequest = false;
296
297 onCancel(() => {
298 cancelOnRequest = true;
299 });
300
301 ee.on('request', req => {
302 if (cancelOnRequest) {
303 req.abort();
304 }
305
306 onCancel(() => {
307 req.abort();
308 });
309
310 if (is.nodeStream(opts.body)) {
311 opts.body.pipe(req);
312 opts.body = undefined;
313 return;
314 }
315
316 req.end(opts.body);
317 });
318
319 ee.on('response', res => {
320 const stream = is.null(opts.encoding) ? getStream.buffer(res) : getStream(res, opts);
321
322 stream
323 .catch(err => reject(new got.ReadError(err, opts)))
324 .then(data => {
325 const statusCode = res.statusCode;
326 const limitStatusCode = opts.followRedirect ? 299 : 399;
327
328 res.body = data;
329
330 if (opts.json && res.body) {
331 try {
332 res.body = JSON.parse(res.body);
333 } catch (err) {
334 if (statusCode >= 200 && statusCode < 300) {
335 throw new got.ParseError(err, statusCode, opts, data);
336 }
337 }
338 }
339
340 if (statusCode !== 304 && (statusCode < 200 || statusCode > limitStatusCode)) {
341 throw new got.HTTPError(statusCode, res.statusMessage, res.headers, opts);
342 }
343
344 resolve(res);
345 })
346 .catch(err => {
347 Object.defineProperty(err, 'response', {value: res});
348 reject(err);
349 });
350 });
351
352 ee.once('error', reject);
353 ee.on('redirect', proxy.emit.bind(proxy, 'redirect'));
354 ee.on('uploadProgress', proxy.emit.bind(proxy, 'uploadProgress'));
355 ee.on('downloadProgress', proxy.emit.bind(proxy, 'downloadProgress'));
356 });
357
358 const promise = timeoutFn(cancelable);
359
360 promise.cancel = cancelable.cancel.bind(cancelable);
361
362 promise.on = (name, fn) => {
363 proxy.on(name, fn);
364 return promise;
365 };
366
367 return promise;
368}
369
370function asStream(opts) {
371 opts.stream = true;
372
373 const input = new PassThrough();
374 const output = new PassThrough();
375 const proxy = duplexer3(input, output);
376 let timeout;
377
378 if (opts.gotTimeout && opts.gotTimeout.request) {
379 timeout = setTimeout(() => {
380 proxy.emit('error', new got.RequestError({message: 'Request timed out', code: 'ETIMEDOUT'}, opts));
381 }, opts.gotTimeout.request);
382 }
383
384 if (opts.json) {
385 throw new Error('Got can not be used as a stream when the `json` option is used');
386 }
387
388 if (opts.body) {
389 proxy.write = () => {
390 throw new Error('Got\'s stream is not writable when the `body` option is used');
391 };
392 }
393
394 const ee = requestAsEventEmitter(opts);
395
396 ee.on('request', req => {
397 proxy.emit('request', req);
398
399 if (is.nodeStream(opts.body)) {
400 opts.body.pipe(req);
401 return;
402 }
403
404 if (opts.body) {
405 req.end(opts.body);
406 return;
407 }
408
409 if (opts.method === 'POST' || opts.method === 'PUT' || opts.method === 'PATCH') {
410 input.pipe(req);
411 return;
412 }
413
414 req.end();
415 });
416
417 ee.on('response', res => {
418 clearTimeout(timeout);
419
420 const statusCode = res.statusCode;
421
422 res.on('error', err => {
423 proxy.emit('error', new got.ReadError(err, opts));
424 });
425
426 res.pipe(output);
427
428 if (statusCode !== 304 && (statusCode < 200 || statusCode > 299)) {
429 proxy.emit('error', new got.HTTPError(statusCode, res.statusMessage, res.headers, opts), null, res);
430 return;
431 }
432
433 proxy.emit('response', res);
434 });
435
436 ee.on('error', proxy.emit.bind(proxy, 'error'));
437 ee.on('redirect', proxy.emit.bind(proxy, 'redirect'));
438 ee.on('uploadProgress', proxy.emit.bind(proxy, 'uploadProgress'));
439 ee.on('downloadProgress', proxy.emit.bind(proxy, 'downloadProgress'));
440
441 return proxy;
442}
443
444function normalizeArguments(url, opts) {
445 if (!is.string(url) && !is.object(url)) {
446 throw new TypeError(`Parameter \`url\` must be a string or object, not ${is(url)}`);
447 } else if (is.string(url)) {
448 url = url.replace(/^unix:/, 'http://$&');
449
450 try {
451 decodeURI(url);
452 } catch (err) {
453 throw new Error('Parameter `url` must contain valid UTF-8 character sequences');
454 }
455
456 url = urlParseLax(url);
457 if (url.auth) {
458 throw new Error('Basic authentication must be done with the `auth` option');
459 }
460 } else if (isURL.lenient(url)) {
461 url = urlToOptions(url);
462 }
463
464 opts = Object.assign(
465 {
466 path: '',
467 retries: 2,
468 cache: false,
469 decompress: true,
470 useElectronNet: false
471 },
472 url,
473 {
474 protocol: url.protocol || 'http:' // Override both null/undefined with default protocol
475 },
476 opts
477 );
478
479 const headers = lowercaseKeys(opts.headers);
480 for (const key of Object.keys(headers)) {
481 if (is.nullOrUndefined(headers[key])) {
482 delete headers[key];
483 }
484 }
485
486 opts.headers = Object.assign({
487 'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`,
488 'accept-encoding': 'gzip,deflate'
489 }, headers);
490
491 const query = opts.query;
492
493 if (query) {
494 if (!is.string(query)) {
495 opts.query = querystring.stringify(query);
496 }
497
498 opts.path = `${opts.path.split('?')[0]}?${opts.query}`;
499 delete opts.query;
500 }
501
502 if (opts.json && is.undefined(opts.headers.accept)) {
503 opts.headers.accept = 'application/json';
504 }
505
506 const body = opts.body;
507 if (is.nullOrUndefined(body)) {
508 opts.method = (opts.method || 'GET').toUpperCase();
509 } else {
510 const headers = opts.headers;
511 if (!is.nodeStream(body) && !is.string(body) && !is.buffer(body) && !(opts.form || opts.json)) {
512 throw new TypeError('The `body` option must be a stream.Readable, string, Buffer or plain Object');
513 }
514
515 const canBodyBeStringified = is.plainObject(body) || is.array(body);
516 if ((opts.form || opts.json) && !canBodyBeStringified) {
517 throw new TypeError('The `body` option must be a plain Object or Array when the `form` or `json` option is used');
518 }
519
520 if (isFormData(body)) {
521 // Special case for https://github.com/form-data/form-data
522 headers['content-type'] = headers['content-type'] || `multipart/form-data; boundary=${body.getBoundary()}`;
523 } else if (opts.form && canBodyBeStringified) {
524 headers['content-type'] = headers['content-type'] || 'application/x-www-form-urlencoded';
525 opts.body = querystring.stringify(body);
526 } else if (opts.json && canBodyBeStringified) {
527 headers['content-type'] = headers['content-type'] || 'application/json';
528 opts.body = JSON.stringify(body);
529 }
530
531 if (is.undefined(headers['content-length']) && is.undefined(headers['transfer-encoding']) && !is.nodeStream(body)) {
532 const length = is.string(opts.body) ? Buffer.byteLength(opts.body) : opts.body.length;
533 headers['content-length'] = length;
534 }
535
536 // Convert buffer to stream to receive upload progress events
537 // see https://github.com/sindresorhus/got/pull/322
538 if (is.buffer(body)) {
539 opts.body = intoStream(body);
540 opts.body._buffer = body;
541 }
542
543 opts.method = (opts.method || 'POST').toUpperCase();
544 }
545
546 if (opts.hostname === 'unix') {
547 const matches = /(.+?):(.+)/.exec(opts.path);
548
549 if (matches) {
550 opts.socketPath = matches[1];
551 opts.path = matches[2];
552 opts.host = null;
553 }
554 }
555
556 if (!is.function(opts.retries)) {
557 const retries = opts.retries;
558
559 opts.retries = (iter, err) => {
560 if (iter > retries || !isRetryAllowed(err)) {
561 return 0;
562 }
563
564 const noise = Math.random() * 100;
565
566 return ((1 << iter) * 1000) + noise;
567 };
568 }
569
570 if (is.undefined(opts.followRedirect)) {
571 opts.followRedirect = true;
572 }
573
574 if (opts.timeout) {
575 if (is.number(opts.timeout)) {
576 opts.gotTimeout = {request: opts.timeout};
577 } else {
578 opts.gotTimeout = opts.timeout;
579 }
580 delete opts.timeout;
581 }
582
583 return opts;
584}
585
586function got(url, opts) {
587 try {
588 return asPromise(normalizeArguments(url, opts));
589 } catch (err) {
590 return Promise.reject(err);
591 }
592}
593
594got.stream = (url, opts) => asStream(normalizeArguments(url, opts));
595
596const methods = [
597 'get',
598 'post',
599 'put',
600 'patch',
601 'head',
602 'delete'
603];
604
605for (const method of methods) {
606 got[method] = (url, opts) => got(url, Object.assign({}, opts, {method}));
607 got.stream[method] = (url, opts) => got.stream(url, Object.assign({}, opts, {method}));
608}
609
610Object.assign(got, errors);
611
612module.exports = got;