follow-redirects 1.15.2 → 1.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of follow-redirects might be problematic. Click here for more details.

Files changed (2) hide show
  1. package/index.js +129 -79
  2. package/package.json +2 -3
package/index.js CHANGED
@@ -6,6 +6,29 @@ var Writable = require("stream").Writable;
6
6
  var assert = require("assert");
7
7
  var debug = require("./debug");
8
8
 
9
+ // Whether to use the native URL object or the legacy url module
10
+ var useNativeURL = false;
11
+ try {
12
+ assert(new URL());
13
+ }
14
+ catch (error) {
15
+ useNativeURL = error.code === "ERR_INVALID_URL";
16
+ }
17
+
18
+ // URL fields to preserve in copy operations
19
+ var preservedUrlFields = [
20
+ "auth",
21
+ "host",
22
+ "hostname",
23
+ "href",
24
+ "path",
25
+ "pathname",
26
+ "port",
27
+ "protocol",
28
+ "query",
29
+ "search",
30
+ ];
31
+
9
32
  // Create handlers that pass events from native requests
10
33
  var events = ["abort", "aborted", "connect", "error", "socket", "timeout"];
11
34
  var eventHandlers = Object.create(null);
@@ -15,19 +38,20 @@ events.forEach(function (event) {
15
38
  };
16
39
  });
17
40
 
41
+ // Error types with codes
18
42
  var InvalidUrlError = createErrorType(
19
43
  "ERR_INVALID_URL",
20
44
  "Invalid URL",
21
45
  TypeError
22
46
  );
23
- // Error types with codes
24
47
  var RedirectionError = createErrorType(
25
48
  "ERR_FR_REDIRECTION_FAILURE",
26
49
  "Redirected request failed"
27
50
  );
28
51
  var TooManyRedirectsError = createErrorType(
29
52
  "ERR_FR_TOO_MANY_REDIRECTS",
30
- "Maximum number of redirects exceeded"
53
+ "Maximum number of redirects exceeded",
54
+ RedirectionError
31
55
  );
32
56
  var MaxBodyLengthExceededError = createErrorType(
33
57
  "ERR_FR_MAX_BODY_LENGTH_EXCEEDED",
@@ -38,6 +62,9 @@ var WriteAfterEndError = createErrorType(
38
62
  "write after end"
39
63
  );
40
64
 
65
+ // istanbul ignore next
66
+ var destroy = Writable.prototype.destroy || noop;
67
+
41
68
  // An HTTP(S) request that can be redirected
42
69
  function RedirectableRequest(options, responseCallback) {
43
70
  // Initialize the request
@@ -59,7 +86,13 @@ function RedirectableRequest(options, responseCallback) {
59
86
  // React to responses of native requests
60
87
  var self = this;
61
88
  this._onNativeResponse = function (response) {
62
- self._processResponse(response);
89
+ try {
90
+ self._processResponse(response);
91
+ }
92
+ catch (cause) {
93
+ self.emit("error", cause instanceof RedirectionError ?
94
+ cause : new RedirectionError({ cause: cause }));
95
+ }
63
96
  };
64
97
 
65
98
  // Perform the first request
@@ -68,10 +101,17 @@ function RedirectableRequest(options, responseCallback) {
68
101
  RedirectableRequest.prototype = Object.create(Writable.prototype);
69
102
 
70
103
  RedirectableRequest.prototype.abort = function () {
71
- abortRequest(this._currentRequest);
104
+ destroyRequest(this._currentRequest);
105
+ this._currentRequest.abort();
72
106
  this.emit("abort");
73
107
  };
74
108
 
109
+ RedirectableRequest.prototype.destroy = function (error) {
110
+ destroyRequest(this._currentRequest, error);
111
+ destroy.call(this, error);
112
+ return this;
113
+ };
114
+
75
115
  // Writes buffered data to the current native request
76
116
  RedirectableRequest.prototype.write = function (data, encoding, callback) {
77
117
  // Writing is not allowed if end has been called
@@ -184,6 +224,7 @@ RedirectableRequest.prototype.setTimeout = function (msecs, callback) {
184
224
  self.removeListener("abort", clearTimer);
185
225
  self.removeListener("error", clearTimer);
186
226
  self.removeListener("response", clearTimer);
227
+ self.removeListener("close", clearTimer);
187
228
  if (callback) {
188
229
  self.removeListener("timeout", callback);
189
230
  }
@@ -210,6 +251,7 @@ RedirectableRequest.prototype.setTimeout = function (msecs, callback) {
210
251
  this.on("abort", clearTimer);
211
252
  this.on("error", clearTimer);
212
253
  this.on("response", clearTimer);
254
+ this.on("close", clearTimer);
213
255
 
214
256
  return this;
215
257
  };
@@ -268,8 +310,7 @@ RedirectableRequest.prototype._performRequest = function () {
268
310
  var protocol = this._options.protocol;
269
311
  var nativeProtocol = this._options.nativeProtocols[protocol];
270
312
  if (!nativeProtocol) {
271
- this.emit("error", new TypeError("Unsupported protocol " + protocol));
272
- return;
313
+ throw new TypeError("Unsupported protocol " + protocol);
273
314
  }
274
315
 
275
316
  // If specified, use the agent corresponding to the protocol
@@ -361,15 +402,14 @@ RedirectableRequest.prototype._processResponse = function (response) {
361
402
  }
362
403
 
363
404
  // The response is a redirect, so abort the current request
364
- abortRequest(this._currentRequest);
405
+ destroyRequest(this._currentRequest);
365
406
  // Discard the remainder of the response to avoid waiting for data
366
407
  response.destroy();
367
408
 
368
409
  // RFC7231§6.4: A client SHOULD detect and intervene
369
410
  // in cyclical redirections (i.e., "infinite" redirection loops).
370
411
  if (++this._redirectCount > this._options.maxRedirects) {
371
- this.emit("error", new TooManyRedirectsError());
372
- return;
412
+ throw new TooManyRedirectsError();
373
413
  }
374
414
 
375
415
  // Store the request headers if applicable
@@ -403,33 +443,23 @@ RedirectableRequest.prototype._processResponse = function (response) {
403
443
  var currentHostHeader = removeMatchingHeaders(/^host$/i, this._options.headers);
404
444
 
405
445
  // If the redirect is relative, carry over the host of the last request
406
- var currentUrlParts = url.parse(this._currentUrl);
446
+ var currentUrlParts = parseUrl(this._currentUrl);
407
447
  var currentHost = currentHostHeader || currentUrlParts.host;
408
448
  var currentUrl = /^\w+:/.test(location) ? this._currentUrl :
409
449
  url.format(Object.assign(currentUrlParts, { host: currentHost }));
410
450
 
411
- // Determine the URL of the redirection
412
- var redirectUrl;
413
- try {
414
- redirectUrl = url.resolve(currentUrl, location);
415
- }
416
- catch (cause) {
417
- this.emit("error", new RedirectionError({ cause: cause }));
418
- return;
419
- }
420
-
421
451
  // Create the redirected request
422
- debug("redirecting to", redirectUrl);
452
+ var redirectUrl = resolveUrl(location, currentUrl);
453
+ debug("redirecting to", redirectUrl.href);
423
454
  this._isRedirect = true;
424
- var redirectUrlParts = url.parse(redirectUrl);
425
- Object.assign(this._options, redirectUrlParts);
455
+ spreadUrlObject(redirectUrl, this._options);
426
456
 
427
457
  // Drop confidential headers when redirecting to a less secure protocol
428
458
  // or to a different domain that is not a superdomain
429
- if (redirectUrlParts.protocol !== currentUrlParts.protocol &&
430
- redirectUrlParts.protocol !== "https:" ||
431
- redirectUrlParts.host !== currentHost &&
432
- !isSubdomain(redirectUrlParts.host, currentHost)) {
459
+ if (redirectUrl.protocol !== currentUrlParts.protocol &&
460
+ redirectUrl.protocol !== "https:" ||
461
+ redirectUrl.host !== currentHost &&
462
+ !isSubdomain(redirectUrl.host, currentHost)) {
433
463
  removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers);
434
464
  }
435
465
 
@@ -444,23 +474,12 @@ RedirectableRequest.prototype._processResponse = function (response) {
444
474
  method: method,
445
475
  headers: requestHeaders,
446
476
  };
447
- try {
448
- beforeRedirect(this._options, responseDetails, requestDetails);
449
- }
450
- catch (err) {
451
- this.emit("error", err);
452
- return;
453
- }
477
+ beforeRedirect(this._options, responseDetails, requestDetails);
454
478
  this._sanitizeOptions(this._options);
455
479
  }
456
480
 
457
481
  // Perform the redirected request
458
- try {
459
- this._performRequest();
460
- }
461
- catch (cause) {
462
- this.emit("error", new RedirectionError({ cause: cause }));
463
- }
482
+ this._performRequest();
464
483
  };
465
484
 
466
485
  // Wraps the key/value object of protocols with redirect functionality
@@ -480,27 +499,16 @@ function wrap(protocols) {
480
499
 
481
500
  // Executes a request, following redirects
482
501
  function request(input, options, callback) {
483
- // Parse parameters
484
- if (isString(input)) {
485
- var parsed;
486
- try {
487
- parsed = urlToOptions(new URL(input));
488
- }
489
- catch (err) {
490
- /* istanbul ignore next */
491
- parsed = url.parse(input);
492
- }
493
- if (!isString(parsed.protocol)) {
494
- throw new InvalidUrlError({ input });
495
- }
496
- input = parsed;
502
+ // Parse parameters, ensuring that input is an object
503
+ if (isURL(input)) {
504
+ input = spreadUrlObject(input);
497
505
  }
498
- else if (URL && (input instanceof URL)) {
499
- input = urlToOptions(input);
506
+ else if (isString(input)) {
507
+ input = spreadUrlObject(parseUrl(input));
500
508
  }
501
509
  else {
502
510
  callback = options;
503
- options = input;
511
+ options = validateUrl(input);
504
512
  input = { protocol: protocol };
505
513
  }
506
514
  if (isFunction(options)) {
@@ -539,27 +547,57 @@ function wrap(protocols) {
539
547
  return exports;
540
548
  }
541
549
 
542
- /* istanbul ignore next */
543
550
  function noop() { /* empty */ }
544
551
 
545
- // from https://github.com/nodejs/node/blob/master/lib/internal/url.js
546
- function urlToOptions(urlObject) {
547
- var options = {
548
- protocol: urlObject.protocol,
549
- hostname: urlObject.hostname.startsWith("[") ?
550
- /* istanbul ignore next */
551
- urlObject.hostname.slice(1, -1) :
552
- urlObject.hostname,
553
- hash: urlObject.hash,
554
- search: urlObject.search,
555
- pathname: urlObject.pathname,
556
- path: urlObject.pathname + urlObject.search,
557
- href: urlObject.href,
558
- };
559
- if (urlObject.port !== "") {
560
- options.port = Number(urlObject.port);
552
+ function parseUrl(input) {
553
+ var parsed;
554
+ /* istanbul ignore else */
555
+ if (useNativeURL) {
556
+ parsed = new URL(input);
557
+ }
558
+ else {
559
+ // Ensure the URL is valid and absolute
560
+ parsed = validateUrl(url.parse(input));
561
+ if (!isString(parsed.protocol)) {
562
+ throw new InvalidUrlError({ input });
563
+ }
564
+ }
565
+ return parsed;
566
+ }
567
+
568
+ function resolveUrl(relative, base) {
569
+ /* istanbul ignore next */
570
+ return useNativeURL ? new URL(relative, base) : parseUrl(url.resolve(base, relative));
571
+ }
572
+
573
+ function validateUrl(input) {
574
+ if (/^\[/.test(input.hostname) && !/^\[[:0-9a-f]+\]$/i.test(input.hostname)) {
575
+ throw new InvalidUrlError({ input: input.href || input });
576
+ }
577
+ if (/^\[/.test(input.host) && !/^\[[:0-9a-f]+\](:\d+)?$/i.test(input.host)) {
578
+ throw new InvalidUrlError({ input: input.href || input });
579
+ }
580
+ return input;
581
+ }
582
+
583
+ function spreadUrlObject(urlObject, target) {
584
+ var spread = target || {};
585
+ for (var key of preservedUrlFields) {
586
+ spread[key] = urlObject[key];
587
+ }
588
+
589
+ // Fix IPv6 hostname
590
+ if (spread.hostname.startsWith("[")) {
591
+ spread.hostname = spread.hostname.slice(1, -1);
592
+ }
593
+ // Ensure port is a number
594
+ if (spread.port !== "") {
595
+ spread.port = Number(spread.port);
561
596
  }
562
- return options;
597
+ // Concatenate path
598
+ spread.path = spread.search ? spread.pathname + spread.search : spread.pathname;
599
+
600
+ return spread;
563
601
  }
564
602
 
565
603
  function removeMatchingHeaders(regex, headers) {
@@ -585,17 +623,25 @@ function createErrorType(code, message, baseClass) {
585
623
 
586
624
  // Attach constructor and set default properties
587
625
  CustomError.prototype = new (baseClass || Error)();
588
- CustomError.prototype.constructor = CustomError;
589
- CustomError.prototype.name = "Error [" + code + "]";
626
+ Object.defineProperties(CustomError.prototype, {
627
+ constructor: {
628
+ value: CustomError,
629
+ enumerable: false,
630
+ },
631
+ name: {
632
+ value: "Error [" + code + "]",
633
+ enumerable: false,
634
+ },
635
+ });
590
636
  return CustomError;
591
637
  }
592
638
 
593
- function abortRequest(request) {
639
+ function destroyRequest(request, error) {
594
640
  for (var event of events) {
595
641
  request.removeListener(event, eventHandlers[event]);
596
642
  }
597
643
  request.on("error", noop);
598
- request.abort();
644
+ request.destroy(error);
599
645
  }
600
646
 
601
647
  function isSubdomain(subdomain, domain) {
@@ -616,6 +662,10 @@ function isBuffer(value) {
616
662
  return typeof value === "object" && ("length" in value);
617
663
  }
618
664
 
665
+ function isURL(value) {
666
+ return URL && value instanceof URL;
667
+ }
668
+
619
669
  // Exports
620
670
  module.exports = wrap({ http: http, https: https });
621
671
  module.exports.wrap = wrap;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "follow-redirects",
3
- "version": "1.15.2",
3
+ "version": "1.15.4",
4
4
  "description": "HTTP and HTTPS modules that follow redirects.",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -11,9 +11,8 @@
11
11
  "node": ">=4.0"
12
12
  },
13
13
  "scripts": {
14
- "test": "npm run lint && npm run mocha",
15
14
  "lint": "eslint *.js test",
16
- "mocha": "nyc mocha"
15
+ "test": "nyc mocha"
17
16
  },
18
17
  "repository": {
19
18
  "type": "git",