UNPKG

6.64 kBJavaScriptView Raw
1'use strict';
2const {URL} = require('url'); // TODO: Use the `URL` global when targeting Node.js 10
3const util = require('util');
4const EventEmitter = require('events');
5const http = require('http');
6const https = require('https');
7const urlLib = require('url');
8const CacheableRequest = require('cacheable-request');
9const is = require('@sindresorhus/is');
10const timer = require('@szmarczak/http-timer');
11const timedOut = require('./timed-out');
12const getBodySize = require('./get-body-size');
13const getResponse = require('./get-response');
14const progress = require('./progress');
15const {GotError, CacheError, UnsupportedProtocolError, MaxRedirectsError, RequestError} = require('./errors');
16
17const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]);
18const allMethodRedirectCodes = new Set([300, 303, 307, 308]);
19
20module.exports = options => {
21 const emitter = new EventEmitter();
22 const requestUrl = options.href || (new URL(options.path, urlLib.format(options))).toString();
23 const redirects = [];
24 const agents = is.object(options.agent) ? options.agent : null;
25 let retryCount = 0;
26 let retryTries = 0;
27 let redirectUrl;
28 let uploadBodySize;
29
30 const setCookie = options.cookieJar ? util.promisify(options.cookieJar.setCookie.bind(options.cookieJar)) : null;
31 const getCookieString = options.cookieJar ? util.promisify(options.cookieJar.getCookieString.bind(options.cookieJar)) : null;
32
33 const get = async options => {
34 const currentUrl = redirectUrl || requestUrl;
35
36 if (options.protocol !== 'http:' && options.protocol !== 'https:') {
37 emitter.emit('error', new UnsupportedProtocolError(options));
38 return;
39 }
40
41 let fn;
42 if (is.function(options.request)) {
43 fn = {request: options.request};
44 } else {
45 fn = options.protocol === 'https:' ? https : http;
46 }
47
48 if (agents) {
49 const protocolName = options.protocol === 'https:' ? 'https' : 'http';
50 options.agent = agents[protocolName] || options.agent;
51 }
52
53 /* istanbul ignore next: electron.net is broken */
54 if (options.useElectronNet && process.versions.electron) {
55 const r = ({x: require})['yx'.slice(1)]; // Trick webpack
56 const electron = r('electron');
57 fn = electron.net || electron.remote.net;
58 }
59
60 if (options.cookieJar) {
61 try {
62 const cookieString = await getCookieString(currentUrl, {});
63
64 if (!is.empty(cookieString)) {
65 options.headers.cookie = cookieString;
66 }
67 } catch (error) {
68 emitter.emit('error', error);
69 }
70 }
71
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 [];
81 }
82
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;
94
95 const rawCookies = response.headers['set-cookie'];
96 if (options.cookieJar && rawCookies) {
97 try {
98 await Promise.all(rawCookies.map(rawCookie => setCookie(rawCookie, response.url)));
99 } catch (error) {
100 emitter.emit('error', error);
101 }
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
108 if (redirectAll || (redirectGet && (options.method === 'GET' || options.method === 'HEAD'))) {
109 response.resume();
110
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 }
116
117 if (redirects.length >= 10) {
118 emitter.emit('error', new MaxRedirectsError(statusCode, redirects, options), null, response);
119 return;
120 }
121
122 const bufferString = Buffer.from(response.headers.location, 'binary').toString();
123 redirectUrl = (new URL(bufferString, urlLib.format(options))).toString();
124
125 try {
126 decodeURI(redirectUrl);
127 } catch (error) {
128 emitter.emit('error', error);
129 return;
130 }
131
132 redirects.push(redirectUrl);
133
134 const redirectOpts = {
135 ...options,
136 ...urlLib.parse(redirectUrl)
137 };
138
139 emitter.emit('redirect', response, redirectOpts);
140
141 await get(redirectOpts);
142 return;
143 }
144
145 try {
146 getResponse(response, options, emitter, redirects);
147 } catch (error) {
148 emitter.emit('error', error);
149 }
150 });
151
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));
157 }
158 });
159
160 cacheReq.once('request', request => {
161 let aborted = false;
162 request.once('abort', _ => {
163 aborted = true;
164 });
165
166 request.once('error', error => {
167 if (aborted) {
168 return;
169 }
170
171 if (!(error instanceof GotError)) {
172 error = new RequestError(error, options);
173 }
174 emitter.emit('retry', error, retried => {
175 if (!retried) {
176 emitter.emit('error', error);
177 }
178 });
179 });
180
181 timings = timer(request);
182
183 progress.upload(request, emitter, uploadBodySize);
184
185 if (options.gotTimeout) {
186 timedOut(request, options);
187 }
188
189 emitter.emit('request', request);
190 });
191 };
192
193 emitter.on('retry', (error, cb) => {
194 let backoff;
195 try {
196 backoff = options.gotRetry.retries(++retryTries, error);
197 } catch (error) {
198 emitter.emit('error', error);
199 return;
200 }
201
202 if (backoff) {
203 retryCount++;
204 setTimeout(get, backoff, options);
205 cb(true);
206 return;
207 }
208
209 cb(false);
210 });
211
212 setImmediate(async () => {
213 try {
214 uploadBodySize = await getBodySize(options);
215
216 if (is.undefined(options.headers['content-length']) && is.undefined(options.headers['transfer-encoding'])) {
217 if (uploadBodySize > 0 || options.method === 'PUT') {
218 options.headers['content-length'] = uploadBodySize;
219 }
220 }
221
222 for (const hook of options.hooks.beforeRequest) {
223 // eslint-disable-next-line no-await-in-loop
224 await hook(options);
225 }
226
227 await get(options);
228 } catch (error) {
229 emitter.emit('error', error);
230 }
231 });
232
233 return emitter;
234};