1 | 'use strict';
|
2 |
|
3 | const URLGlobal = typeof URL === 'undefined' ? require('url').URL : URL;
|
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 timedOut = require('./timed-out');
|
11 | const getBodySize = require('./get-body-size');
|
12 | const getResponse = require('./get-response');
|
13 | const progress = require('./progress');
|
14 | const {GotError, CacheError, UnsupportedProtocolError, MaxRedirectsError, RequestError} = require('./errors');
|
15 |
|
16 | const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]);
|
17 | const allMethodRedirectCodes = new Set([300, 303, 307, 308]);
|
18 |
|
19 | module.exports = options => {
|
20 | const emitter = new EventEmitter();
|
21 | const requestUrl = options.href || (new URLGlobal(options.path, urlLib.format(options))).toString();
|
22 | const redirects = [];
|
23 | const agents = is.object(options.agent) ? options.agent : null;
|
24 | let retryCount = 0;
|
25 | let retryTries = 0;
|
26 | let redirectUrl;
|
27 | let uploadBodySize;
|
28 |
|
29 | const get = options => {
|
30 | if (options.protocol !== 'http:' && options.protocol !== 'https:') {
|
31 | emitter.emit('error', new UnsupportedProtocolError(options));
|
32 | return;
|
33 | }
|
34 |
|
35 | let fn = options.protocol === 'https:' ? https : http;
|
36 |
|
37 | if (agents) {
|
38 | const protocolName = options.protocol === 'https:' ? 'https' : 'http';
|
39 | options.agent = agents[protocolName] || options.agent;
|
40 | }
|
41 |
|
42 |
|
43 | if (options.useElectronNet && process.versions.electron) {
|
44 | const electron = global['require']('electron');
|
45 | fn = electron.net || electron.remote.net;
|
46 | }
|
47 |
|
48 | const cacheableRequest = new CacheableRequest(fn.request, options.cache);
|
49 | const cacheReq = cacheableRequest(options, response => {
|
50 | const {statusCode} = response;
|
51 | response.retryCount = retryCount;
|
52 | response.url = redirectUrl || requestUrl;
|
53 | response.requestUrl = requestUrl;
|
54 |
|
55 | const followRedirect = options.followRedirect && 'location' in response.headers;
|
56 | const redirectGet = followRedirect && getMethodRedirectCodes.has(statusCode);
|
57 | const redirectAll = followRedirect && allMethodRedirectCodes.has(statusCode);
|
58 |
|
59 | if (redirectAll || (redirectGet && (options.method === 'GET' || options.method === 'HEAD'))) {
|
60 | response.resume();
|
61 |
|
62 | if (statusCode === 303) {
|
63 |
|
64 |
|
65 | options.method = 'GET';
|
66 | }
|
67 |
|
68 | if (redirects.length >= 10) {
|
69 | emitter.emit('error', new MaxRedirectsError(statusCode, redirects, options), null, response);
|
70 | return;
|
71 | }
|
72 |
|
73 | const bufferString = Buffer.from(response.headers.location, 'binary').toString();
|
74 | redirectUrl = (new URLGlobal(bufferString, urlLib.format(options))).toString();
|
75 |
|
76 | try {
|
77 | decodeURI(redirectUrl);
|
78 | } catch (error) {
|
79 | emitter.emit('error', error);
|
80 | return;
|
81 | }
|
82 |
|
83 | redirects.push(redirectUrl);
|
84 |
|
85 | const redirectOpts = {
|
86 | ...options,
|
87 | ...urlLib.parse(redirectUrl)
|
88 | };
|
89 |
|
90 | emitter.emit('redirect', response, redirectOpts);
|
91 |
|
92 | get(redirectOpts);
|
93 | return;
|
94 | }
|
95 |
|
96 | try {
|
97 | getResponse(response, options, emitter, redirects);
|
98 | } catch (error) {
|
99 | emitter.emit('error', error);
|
100 | }
|
101 | });
|
102 |
|
103 | cacheReq.on('error', error => {
|
104 | if (error instanceof CacheableRequest.RequestError) {
|
105 | emitter.emit('error', new RequestError(error, options));
|
106 | } else {
|
107 | emitter.emit('error', new CacheError(error, options));
|
108 | }
|
109 | });
|
110 |
|
111 | cacheReq.once('request', request => {
|
112 | let aborted = false;
|
113 | request.once('abort', _ => {
|
114 | aborted = true;
|
115 | });
|
116 |
|
117 | request.once('error', error => {
|
118 | if (aborted) {
|
119 | return;
|
120 | }
|
121 |
|
122 | if (!(error instanceof GotError)) {
|
123 | error = new RequestError(error, options);
|
124 | }
|
125 | emitter.emit('retry', error, retried => {
|
126 | if (!retried) {
|
127 | emitter.emit('error', error);
|
128 | }
|
129 | });
|
130 | });
|
131 |
|
132 | progress.upload(request, emitter, uploadBodySize);
|
133 |
|
134 | if (options.gotTimeout) {
|
135 | timedOut(request, options);
|
136 | }
|
137 |
|
138 | emitter.emit('request', request);
|
139 | });
|
140 | };
|
141 |
|
142 | emitter.on('retry', (error, cb) => {
|
143 | let backoff;
|
144 | try {
|
145 | backoff = options.gotRetry.retries(++retryTries, error);
|
146 | } catch (error) {
|
147 | emitter.emit('error', error);
|
148 | return;
|
149 | }
|
150 |
|
151 | if (backoff) {
|
152 | retryCount++;
|
153 | setTimeout(get, backoff, options);
|
154 | cb(true);
|
155 | return;
|
156 | }
|
157 |
|
158 | cb(false);
|
159 | });
|
160 |
|
161 | setImmediate(async () => {
|
162 | try {
|
163 | uploadBodySize = await getBodySize(options);
|
164 |
|
165 | if (
|
166 | uploadBodySize > 0 &&
|
167 | is.undefined(options.headers['content-length']) &&
|
168 | is.undefined(options.headers['transfer-encoding'])
|
169 | ) {
|
170 | options.headers['content-length'] = uploadBodySize;
|
171 | }
|
172 |
|
173 | for (const hook of options.hooks.beforeRequest) {
|
174 |
|
175 | await hook(options);
|
176 | }
|
177 |
|
178 | get(options);
|
179 | } catch (error) {
|
180 | emitter.emit('error', error);
|
181 | }
|
182 | });
|
183 |
|
184 | return emitter;
|
185 | };
|