1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const JSONParse = require("parse-json");
|
4 | const uri = require("url");
|
5 | const util = require("util");
|
6 | const deps_1 = require("./deps");
|
7 | const pjson = require('../package.json');
|
8 | const debug = require('debug')('http');
|
9 | const debugHeaders = require('debug')('http:headers');
|
10 | function concat(stream) {
|
11 | return new Promise(resolve => {
|
12 | let strings = [];
|
13 | stream.on('data', data => strings.push(data));
|
14 | stream.on('end', () => resolve(strings.join('')));
|
15 | });
|
16 | }
|
17 | function caseInsensitiveObject() {
|
18 | let lowercaseKey = (k) => (typeof k === 'string' ? k.toLowerCase() : k);
|
19 | return new Proxy({}, {
|
20 | get: (t, k) => {
|
21 | k = lowercaseKey(k);
|
22 | return t[k];
|
23 | },
|
24 | set: (t, k, v) => {
|
25 | k = lowercaseKey(k);
|
26 | t[k] = v;
|
27 | return true;
|
28 | },
|
29 | deleteProperty: (t, k) => {
|
30 | k = lowercaseKey(k);
|
31 | if (k in t)
|
32 | return false;
|
33 | return delete t[k];
|
34 | },
|
35 | has: (t, k) => {
|
36 | k = lowercaseKey(k);
|
37 | return k in t;
|
38 | },
|
39 | });
|
40 | }
|
41 | function lowercaseHeaders(headers) {
|
42 | let newHeaders = caseInsensitiveObject();
|
43 | for (let k of Object.keys(headers)) {
|
44 | if (!headers[k] && headers[k] !== '')
|
45 | continue;
|
46 | newHeaders[k] = headers[k];
|
47 | }
|
48 | return newHeaders;
|
49 | }
|
50 |
|
51 |
|
52 |
|
53 |
|
54 | class HTTP {
|
55 | constructor(url, options = {}) {
|
56 | this._redirectRetries = 0;
|
57 | this._errorRetries = 0;
|
58 | const userAgent = (global['http-call'] && global['http-call'].userAgent && global['http-call'].userAgent) ||
|
59 | `${pjson.name}/${pjson.version} node-${process.version}`;
|
60 | this.options = Object.assign({}, this.ctor.defaults, options, { headers: lowercaseHeaders(Object.assign({ 'user-agent': userAgent }, this.ctor.defaults.headers, options.headers)) });
|
61 | if (!url)
|
62 | throw new Error('no url provided');
|
63 | this.url = url;
|
64 | if (this.options.body)
|
65 | this._parseBody(this.options.body);
|
66 | }
|
67 | static create(options = {}) {
|
68 | var _a;
|
69 | const defaults = this.defaults;
|
70 | return _a = class CustomHTTP extends HTTP {
|
71 | },
|
72 | _a.defaults = Object.assign({}, defaults, options),
|
73 | _a;
|
74 | }
|
75 | |
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 | static get(url, options = {}) {
|
87 | return this.request(url, Object.assign({}, options, { method: 'GET' }));
|
88 | }
|
89 | |
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 | static post(url, options = {}) {
|
101 | return this.request(url, Object.assign({}, options, { method: 'POST' }));
|
102 | }
|
103 | |
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 | static put(url, options = {}) {
|
115 | return this.request(url, Object.assign({}, options, { method: 'PUT' }));
|
116 | }
|
117 | |
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 | static async patch(url, options = {}) {
|
129 | return this.request(url, Object.assign({}, options, { method: 'PATCH' }));
|
130 | }
|
131 | |
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 | static async delete(url, options = {}) {
|
143 | return this.request(url, Object.assign({}, options, { method: 'DELETE' }));
|
144 | }
|
145 | |
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 | static stream(url, options = {}) {
|
158 | return this.request(url, Object.assign({}, options, { raw: true }));
|
159 | }
|
160 | static async request(url, options = {}) {
|
161 | let http = new this(url, options);
|
162 | await http._request();
|
163 | return http;
|
164 | }
|
165 | get method() {
|
166 | return this.options.method || 'GET';
|
167 | }
|
168 | get statusCode() {
|
169 | if (!this.response)
|
170 | return 0;
|
171 | return this.response.statusCode || 0;
|
172 | }
|
173 | get secure() {
|
174 | return this.options.protocol === 'https:';
|
175 | }
|
176 | get url() {
|
177 | return `${this.options.protocol}//${this.options.host}${this.options.path}`;
|
178 | }
|
179 | set url(input) {
|
180 | let u = uri.parse(input);
|
181 | this.options.protocol = u.protocol || this.options.protocol;
|
182 | this.options.host = u.hostname || this.ctor.defaults.host || 'localhost';
|
183 | this.options.path = u.path || '/';
|
184 | this.options.agent = this.options.agent || deps_1.deps.proxy.agent(this.secure, this.options.host);
|
185 | this.options.port = u.port || this.options.port || (this.secure ? 443 : 80);
|
186 | }
|
187 | get headers() {
|
188 | if (!this.response)
|
189 | return {};
|
190 | return this.response.headers;
|
191 | }
|
192 | get partial() {
|
193 | if (this.method !== 'GET' || this.options.partial)
|
194 | return true;
|
195 | return !(this.headers['next-range'] && this.body instanceof Array);
|
196 | }
|
197 | get ctor() {
|
198 | return this.constructor;
|
199 | }
|
200 | async _request() {
|
201 | this._debugRequest();
|
202 | try {
|
203 | this.response = await this._performRequest();
|
204 | }
|
205 | catch (err) {
|
206 | debug(err);
|
207 | return this._maybeRetry(err);
|
208 | }
|
209 | if (this._shouldParseResponseBody)
|
210 | await this._parse();
|
211 | this._debugResponse();
|
212 | if (this._responseRedirect)
|
213 | return this._redirect();
|
214 | if (!this._responseOK) {
|
215 | throw new HTTPError(this);
|
216 | }
|
217 | if (!this.partial)
|
218 | await this._getNextRange();
|
219 | }
|
220 | async _redirect() {
|
221 | this._redirectRetries++;
|
222 | if (this._redirectRetries > 10)
|
223 | throw new Error(`Redirect loop at ${this.url}`);
|
224 | if (!this.headers.location)
|
225 | throw new Error(`Redirect from ${this.url} has no location header`);
|
226 | const location = this.headers.location;
|
227 | if (Array.isArray(location)) {
|
228 | this.url = location[0];
|
229 | }
|
230 | else {
|
231 | this.url = location;
|
232 | }
|
233 | await this._request();
|
234 | }
|
235 | async _maybeRetry(err) {
|
236 | this._errorRetries++;
|
237 | const allowed = (err) => {
|
238 | if (this._errorRetries > 5)
|
239 | return false;
|
240 | if (!err || !err.code)
|
241 | return false;
|
242 | if (err.code === 'ENOTFOUND')
|
243 | return true;
|
244 | return require('is-retry-allowed')(err);
|
245 | };
|
246 | if (allowed(err)) {
|
247 | let noise = Math.random() * 100;
|
248 |
|
249 | await this._wait((1 << this._errorRetries) * 100 + noise);
|
250 | await this._request();
|
251 | return;
|
252 | }
|
253 | throw err;
|
254 | }
|
255 | get _chalk() {
|
256 | try {
|
257 | return require('chalk');
|
258 | }
|
259 | catch (err) {
|
260 | return;
|
261 | }
|
262 | }
|
263 | _renderStatus(code) {
|
264 | if (code < 200)
|
265 | return code;
|
266 | if (code < 300)
|
267 | return this._chalk.green(code);
|
268 | if (code < 400)
|
269 | return this._chalk.bold.cyan(code);
|
270 | if (code < 500)
|
271 | return this._chalk.bgYellow(code);
|
272 | if (code < 600)
|
273 | return this._chalk.bgRed(code);
|
274 | return code;
|
275 | }
|
276 | _debugRequest() {
|
277 | if (!debug.enabled)
|
278 | return;
|
279 | let output = [`${this._chalk.bold('→')} ${this._chalk.blue.bold(this.options.method)} ${this._chalk.bold(this.url)}`];
|
280 | if (this.options.agent)
|
281 | output.push(` proxy: ${util.inspect(this.options.agent)}`);
|
282 | if (debugHeaders.enabled)
|
283 | output.push(this._renderHeaders(this.options.headers));
|
284 | if (this.options.body)
|
285 | output.push(this.options.body);
|
286 | debug(output.join('\n'));
|
287 | }
|
288 | _debugResponse() {
|
289 | if (!debug.enabled)
|
290 | return;
|
291 | const chalk = require('chalk');
|
292 | let output = [`${this._chalk.white.bold('←')} ${this._chalk.blue.bold(this.method)} ${this._chalk.bold(this.url)} ${this._renderStatus(this.statusCode)}`];
|
293 | if (debugHeaders.enabled)
|
294 | output.push(this._renderHeaders(this.headers));
|
295 | if (this.body)
|
296 | output.push(util.inspect(this.body));
|
297 | debug(output.join('\n'));
|
298 | }
|
299 | _renderHeaders(headers) {
|
300 | headers = Object.assign({}, headers);
|
301 | if (process.env.HTTP_CALL_REDACT !== '0' && headers.authorization)
|
302 | headers.authorization = '[REDACTED]';
|
303 | return Object.entries(headers)
|
304 | .sort(([a], [b]) => {
|
305 | if (a < b)
|
306 | return -1;
|
307 | if (a > b)
|
308 | return 1;
|
309 | return 0;
|
310 | })
|
311 | .map(([k, v]) => ` ${this._chalk.dim(k + ':')} ${this._chalk.cyan(util.inspect(v))}`)
|
312 | .join('\n');
|
313 | }
|
314 | _performRequest() {
|
315 | return new Promise((resolve, reject) => {
|
316 | if (this.secure) {
|
317 | this.request = deps_1.deps.https.request(this.options, resolve);
|
318 | }
|
319 | else {
|
320 | this.request = deps_1.deps.http.request(this.options, resolve);
|
321 | }
|
322 | if (this.options.timeout) {
|
323 | this.request.setTimeout(this.options.timeout, () => {
|
324 | debug(`← ${this.method} ${this.url} TIMED OUT`);
|
325 | this.request.abort();
|
326 | });
|
327 | }
|
328 | this.request.on('error', reject);
|
329 | this.request.on('timeout', reject);
|
330 | if (this.options.body && deps_1.deps.isStream.readable(this.options.body)) {
|
331 | this.options.body.pipe(this.request);
|
332 | }
|
333 | else {
|
334 | this.request.end(this.options.body);
|
335 | }
|
336 | });
|
337 | }
|
338 | async _parse() {
|
339 | this.body = await concat(this.response);
|
340 | let type = this.response.headers['content-type'] ? deps_1.deps.contentType.parse(this.response).type : '';
|
341 | let json = type.startsWith('application/json') || type.endsWith('+json');
|
342 | if (json)
|
343 | this.body = JSONParse(this.body);
|
344 | }
|
345 | _parseBody(body) {
|
346 | if (deps_1.deps.isStream.readable(body)) {
|
347 | this.options.body = body;
|
348 | return;
|
349 | }
|
350 | if (!this.options.headers['content-type']) {
|
351 | this.options.headers['content-type'] = 'application/json';
|
352 | }
|
353 | if (this.options.headers['content-type'] === 'application/json') {
|
354 | this.options.body = JSON.stringify(body);
|
355 | }
|
356 | else {
|
357 | this.options.body = body;
|
358 | }
|
359 | this.options.headers['content-length'] = Buffer.byteLength(this.options.body).toString();
|
360 | }
|
361 | async _getNextRange() {
|
362 | const next = this.headers['next-range'];
|
363 | this.options.headers.range = Array.isArray(next) ? next[0] : next;
|
364 | let prev = this.body;
|
365 | await this._request();
|
366 | this.body = prev.concat(this.body);
|
367 | }
|
368 | get _responseOK() {
|
369 | if (!this.response)
|
370 | return false;
|
371 | return this.statusCode >= 200 && this.statusCode < 300;
|
372 | }
|
373 | get _responseRedirect() {
|
374 | if (!this.response)
|
375 | return false;
|
376 | return [301, 302, 303, 307, 308].includes(this.statusCode);
|
377 | }
|
378 | get _shouldParseResponseBody() {
|
379 | return !this._responseOK || (!this.options.raw && this._responseOK);
|
380 | }
|
381 | _wait(ms) {
|
382 | return new Promise(resolve => setTimeout(resolve, ms));
|
383 | }
|
384 | }
|
385 | HTTP.defaults = {
|
386 | method: 'GET',
|
387 | host: 'localhost',
|
388 | protocol: 'https:',
|
389 | path: '/',
|
390 | raw: false,
|
391 | partial: false,
|
392 | headers: {},
|
393 | };
|
394 | exports.HTTP = HTTP;
|
395 | exports.default = HTTP;
|
396 | class HTTPError extends Error {
|
397 | constructor(http) {
|
398 | super();
|
399 | this.__httpcall = pjson.version;
|
400 | if (typeof http.body === 'string' || typeof http.body.message === 'string')
|
401 | this.message = http.body.message || http.body;
|
402 | else
|
403 | this.message = util.inspect(http.body);
|
404 | this.message = `HTTP Error ${http.statusCode} for ${http.method} ${http.url}\n${this.message}`;
|
405 | this.statusCode = http.statusCode;
|
406 | this.http = http;
|
407 | this.body = http.body;
|
408 | }
|
409 | }
|
410 | exports.HTTPError = HTTPError;
|