123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- 'use strict';
- const {URL} = require('url'); // TODO: Use the `URL` global when targeting Node.js 10
- const util = require('util');
- const EventEmitter = require('events');
- const http = require('http');
- const https = require('https');
- const urlLib = require('url');
- const CacheableRequest = require('cacheable-request');
- const toReadableStream = require('to-readable-stream');
- const is = require('@sindresorhus/is');
- const timer = require('@szmarczak/http-timer');
- const timedOut = require('./utils/timed-out');
- const getBodySize = require('./utils/get-body-size');
- const getResponse = require('./get-response');
- const progress = require('./progress');
- const {CacheError, UnsupportedProtocolError, MaxRedirectsError, RequestError, TimeoutError} = require('./errors');
- const urlToOptions = require('./utils/url-to-options');
- const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]);
- const allMethodRedirectCodes = new Set([300, 303, 307, 308]);
- module.exports = (options, input) => {
- const emitter = new EventEmitter();
- const redirects = [];
- let currentRequest;
- let requestUrl;
- let redirectString;
- let uploadBodySize;
- let retryCount = 0;
- let shouldAbort = false;
- const setCookie = options.cookieJar ? util.promisify(options.cookieJar.setCookie.bind(options.cookieJar)) : null;
- const getCookieString = options.cookieJar ? util.promisify(options.cookieJar.getCookieString.bind(options.cookieJar)) : null;
- const agents = is.object(options.agent) ? options.agent : null;
- const emitError = async error => {
- try {
- for (const hook of options.hooks.beforeError) {
- // eslint-disable-next-line no-await-in-loop
- error = await hook(error);
- }
- emitter.emit('error', error);
- } catch (error2) {
- emitter.emit('error', error2);
- }
- };
- const get = async options => {
- const currentUrl = redirectString || requestUrl;
- if (options.protocol !== 'http:' && options.protocol !== 'https:') {
- throw new UnsupportedProtocolError(options);
- }
- decodeURI(currentUrl);
- let fn;
- if (is.function(options.request)) {
- fn = {request: options.request};
- } else {
- fn = options.protocol === 'https:' ? https : http;
- }
- if (agents) {
- const protocolName = options.protocol === 'https:' ? 'https' : 'http';
- options.agent = agents[protocolName] || options.agent;
- }
- /* istanbul ignore next: electron.net is broken */
- if (options.useElectronNet && process.versions.electron) {
- const r = ({x: require})['yx'.slice(1)]; // Trick webpack
- const electron = r('electron');
- fn = electron.net || electron.remote.net;
- }
- if (options.cookieJar) {
- const cookieString = await getCookieString(currentUrl, {});
- if (is.nonEmptyString(cookieString)) {
- options.headers.cookie = cookieString;
- }
- }
- let timings;
- const handleResponse = async response => {
- try {
- /* istanbul ignore next: fixes https://github.com/electron/electron/blob/cbb460d47628a7a146adf4419ed48550a98b2923/lib/browser/api/net.js#L59-L65 */
- if (options.useElectronNet) {
- response = new Proxy(response, {
- get: (target, name) => {
- if (name === 'trailers' || name === 'rawTrailers') {
- return [];
- }
- const value = target[name];
- return is.function(value) ? value.bind(target) : value;
- }
- });
- }
- const {statusCode} = response;
- response.url = currentUrl;
- response.requestUrl = requestUrl;
- response.retryCount = retryCount;
- response.timings = timings;
- response.redirectUrls = redirects;
- response.request = {
- gotOptions: options
- };
- const rawCookies = response.headers['set-cookie'];
- if (options.cookieJar && rawCookies) {
- await Promise.all(rawCookies.map(rawCookie => setCookie(rawCookie, response.url)));
- }
- if (options.followRedirect && 'location' in response.headers) {
- if (allMethodRedirectCodes.has(statusCode) || (getMethodRedirectCodes.has(statusCode) && (options.method === 'GET' || options.method === 'HEAD'))) {
- response.resume(); // We're being redirected, we don't care about the response.
- if (statusCode === 303) {
- // Server responded with "see other", indicating that the resource exists at another location,
- // and the client should request it from that location via GET or HEAD.
- options.method = 'GET';
- }
- if (redirects.length >= 10) {
- throw new MaxRedirectsError(statusCode, redirects, options);
- }
- // Handles invalid URLs. See https://github.com/sindresorhus/got/issues/604
- const redirectBuffer = Buffer.from(response.headers.location, 'binary').toString();
- const redirectURL = new URL(redirectBuffer, currentUrl);
- redirectString = redirectURL.toString();
- redirects.push(redirectString);
- const redirectOptions = {
- ...options,
- ...urlToOptions(redirectURL)
- };
- for (const hook of options.hooks.beforeRedirect) {
- // eslint-disable-next-line no-await-in-loop
- await hook(redirectOptions);
- }
- emitter.emit('redirect', response, redirectOptions);
- await get(redirectOptions);
- return;
- }
- }
- getResponse(response, options, emitter);
- } catch (error) {
- emitError(error);
- }
- };
- const handleRequest = request => {
- if (shouldAbort) {
- request.once('error', () => {});
- request.abort();
- return;
- }
- currentRequest = request;
- request.once('error', error => {
- if (request.aborted) {
- return;
- }
- if (error instanceof timedOut.TimeoutError) {
- error = new TimeoutError(error, options);
- } else {
- error = new RequestError(error, options);
- }
- if (emitter.retry(error) === false) {
- emitError(error);
- }
- });
- timings = timer(request);
- progress.upload(request, emitter, uploadBodySize);
- if (options.gotTimeout) {
- timedOut(request, options.gotTimeout, options);
- }
- emitter.emit('request', request);
- const uploadComplete = () => {
- request.emit('upload-complete');
- };
- try {
- if (is.nodeStream(options.body)) {
- options.body.once('end', uploadComplete);
- options.body.pipe(request);
- options.body = undefined;
- } else if (options.body) {
- request.end(options.body, uploadComplete);
- } else if (input && (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH')) {
- input.once('end', uploadComplete);
- input.pipe(request);
- } else {
- request.end(uploadComplete);
- }
- } catch (error) {
- emitError(new RequestError(error, options));
- }
- };
- if (options.cache) {
- const cacheableRequest = new CacheableRequest(fn.request, options.cache);
- const cacheRequest = cacheableRequest(options, handleResponse);
- cacheRequest.once('error', error => {
- if (error instanceof CacheableRequest.RequestError) {
- emitError(new RequestError(error, options));
- } else {
- emitError(new CacheError(error, options));
- }
- });
- cacheRequest.once('request', handleRequest);
- } else {
- // Catches errors thrown by calling fn.request(...)
- try {
- handleRequest(fn.request(options, handleResponse));
- } catch (error) {
- emitError(new RequestError(error, options));
- }
- }
- };
- emitter.retry = error => {
- let backoff;
- try {
- backoff = options.retry.retries(++retryCount, error);
- } catch (error2) {
- emitError(error2);
- return;
- }
- if (backoff) {
- const retry = async options => {
- try {
- for (const hook of options.hooks.beforeRetry) {
- // eslint-disable-next-line no-await-in-loop
- await hook(options, error, retryCount);
- }
- await get(options);
- } catch (error) {
- emitError(error);
- }
- };
- setTimeout(retry, backoff, {...options, forceRefresh: true});
- return true;
- }
- return false;
- };
- emitter.abort = () => {
- if (currentRequest) {
- currentRequest.once('error', () => {});
- currentRequest.abort();
- } else {
- shouldAbort = true;
- }
- };
- setImmediate(async () => {
- try {
- // Convert buffer to stream to receive upload progress events (#322)
- const {body} = options;
- if (is.buffer(body)) {
- options.body = toReadableStream(body);
- uploadBodySize = body.length;
- } else {
- uploadBodySize = await getBodySize(options);
- }
- if (is.undefined(options.headers['content-length']) && is.undefined(options.headers['transfer-encoding'])) {
- if ((uploadBodySize > 0 || options.method === 'PUT') && !is.null(uploadBodySize)) {
- options.headers['content-length'] = uploadBodySize;
- }
- }
- for (const hook of options.hooks.beforeRequest) {
- // eslint-disable-next-line no-await-in-loop
- await hook(options);
- }
- requestUrl = options.href || (new URL(options.path, urlLib.format(options))).toString();
- await get(options);
- } catch (error) {
- emitError(error);
- }
- });
- return emitter;
- };
|