UNPKG

5.18 kBJavaScriptView Raw
1'use strict';
2/* istanbul ignore next: compatibility reason */
3const URLGlobal = typeof URL === 'undefined' ? require('url').URL : URL; // TODO: Use the `URL` global when targeting Node.js 10
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 timedOut = require('./timed-out');
11const getBodySize = require('./get-body-size');
12const getResponse = require('./get-response');
13const progress = require('./progress');
14const {GotError, CacheError, UnsupportedProtocolError, MaxRedirectsError, RequestError} = require('./errors');
15
16const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]);
17const allMethodRedirectCodes = new Set([300, 303, 307, 308]);
18
19module.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 /* istanbul ignore next: electron.net is broken */
43 if (options.useElectronNet && process.versions.electron) {
44 const electron = global['require']('electron'); // eslint-disable-line dot-notation
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 // Server responded with "see other", indicating that the resource exists at another location,
64 // and the client should request it from that location via GET or HEAD.
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 // eslint-disable-next-line no-await-in-loop
175 await hook(options);
176 }
177
178 get(options);
179 } catch (error) {
180 emitter.emit('error', error);
181 }
182 });
183
184 return emitter;
185};