1 | 'use strict';
|
2 | const {URL} = require('url');
|
3 | const util = require('util');
|
4 | const EventEmitter = require('events');
|
5 | const http = require('http');
|
6 | const https = require('https');
|
7 | const urlLib = require('url');
|
8 | const CacheableRequest = require('cacheable-request');
|
9 | const is = require('@sindresorhus/is');
|
10 | const timer = require('@szmarczak/http-timer');
|
11 | const timedOut = require('./timed-out');
|
12 | const getBodySize = require('./get-body-size');
|
13 | const getResponse = require('./get-response');
|
14 | const progress = require('./progress');
|
15 | const {GotError, CacheError, UnsupportedProtocolError, MaxRedirectsError, RequestError} = require('./errors');
|
16 |
|
17 | const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]);
|
18 | const allMethodRedirectCodes = new Set([300, 303, 307, 308]);
|
19 |
|
20 | module.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 |
|
54 | if (options.useElectronNet && process.versions.electron) {
|
55 | const r = ({x: require})['yx'.slice(1)];
|
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 |
|
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 |
|
113 |
|
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 |
|
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 | };
|