From bcbb096b32686ecad6cd34235358ed6f2217d4f0 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Mon, 25 Sep 2023 10:10:10 +0200 Subject: [PATCH 1/7] Do not directly set Error properties. Fixes https://github.com/follow-redirects/follow-redirects/issues/231 --- index.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 057c6b1..0818005 100644 --- a/index.js +++ b/index.js @@ -597,8 +597,16 @@ function createErrorType(code, message, baseClass) { // Attach constructor and set default properties CustomError.prototype = new (baseClass || Error)(); - CustomError.prototype.constructor = CustomError; - CustomError.prototype.name = "Error [" + code + "]"; + Object.defineProperties(CustomError.prototype, { + constructor: { + value: CustomError, + enumerable: false, + }, + name: { + value: "Error [" + code + "]", + enumerable: false, + }, + }); return CustomError; } From 3d42aecdca39b144a0a2f27ea134b4cf67dd796a Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Fri, 29 Dec 2023 11:13:00 +0100 Subject: [PATCH 2/7] Add bracket tests. --- test/test.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/test/test.js b/test/test.js index 078926d..aa62537 100644 --- a/test/test.js +++ b/test/test.js @@ -177,6 +177,70 @@ describe("follow-redirects", function () { }); }); + it("http.get to IPv4 address", function () { + app.get("/a", redirectsTo("/b")); + app.get("/b", redirectsTo("/c")); + app.get("/c", redirectsTo("/d")); + app.get("/d", redirectsTo("/e")); + app.get("/e", redirectsTo("/f")); + app.get("/f", sendsJson({ a: "b" })); + + return server.start(app) + .then(asPromise(function (resolve, reject) { + http.get("http://127.0.0.1:3600/a", concatJson(resolve, reject)).on("error", reject); + })) + .then(function (res) { + assert.deepEqual(res.parsedJson, { a: "b" }); + assert.deepEqual(res.responseUrl, "http://127.0.0.1:3600/f"); + }); + }); + + it("http.get to IPv6 address", function () { + app.get("/a", redirectsTo("/b")); + app.get("/b", redirectsTo("/c")); + app.get("/c", redirectsTo("/d")); + app.get("/d", redirectsTo("/e")); + app.get("/e", redirectsTo("/f")); + app.get("/f", sendsJson({ a: "b" })); + + return server.start(app) + .then(asPromise(function (resolve, reject) { + http.get("http://[::1]:3600/a", concatJson(resolve, reject)).on("error", reject); + })) + .then(function (res) { + assert.deepEqual(res.parsedJson, { a: "b" }); + assert.deepEqual(res.responseUrl, "http://[::1]:3600/f"); + }); + }); + + it("http.get redirecting to IPv4 address", function () { + app.get("/a", redirectsTo("http://127.0.0.1:3600/b")); + app.get("/b", sendsJson({ a: "b" })); + + return server.start(app) + .then(asPromise(function (resolve, reject) { + http.get("http://localhost:3600/a", concatJson(resolve, reject)).on("error", reject); + })) + .then(function (res) { + assert.deepEqual(res.parsedJson, { a: "b" }); + assert.deepEqual(res.responseUrl, "http://127.0.0.1:3600/b"); + }); + }); + + it("http.get redirecting to IPv6 address", function () { + app.get("/a", redirectsTo("http://[::1]:3600/b")); + app.get("/b", sendsJson({ a: "b" })); + + return server.start(app) + .then(asPromise(function (resolve, reject) { + http.get("http://localhost:3600/a", concatJson(resolve, reject)).on("error", reject); + })) + .then(function (res) { + assert.deepEqual(res.parsedJson, { a: "b" }); + assert.deepEqual(res.responseUrl, "http://[::1]:3600/b"); + }); + }); + it("http.get with response event", function () { app.get("/a", redirectsTo("/b")); app.get("/b", redirectsTo("/c")); From 72bc2a4229bc18dc9fbd57c60579713e6264cb92 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Sat, 30 Dec 2023 10:32:46 +0100 Subject: [PATCH 3/7] Simplify _processResponse error handling. --- index.js | 41 ++++++++++++++--------------------------- test/test.js | 12 +++++++++--- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/index.js b/index.js index 0818005..30fc38c 100644 --- a/index.js +++ b/index.js @@ -27,7 +27,8 @@ var RedirectionError = createErrorType( ); var TooManyRedirectsError = createErrorType( "ERR_FR_TOO_MANY_REDIRECTS", - "Maximum number of redirects exceeded" + "Maximum number of redirects exceeded", + RedirectionError ); var MaxBodyLengthExceededError = createErrorType( "ERR_FR_MAX_BODY_LENGTH_EXCEEDED", @@ -62,7 +63,13 @@ function RedirectableRequest(options, responseCallback) { // React to responses of native requests var self = this; this._onNativeResponse = function (response) { - self._processResponse(response); + try { + self._processResponse(response); + } + catch (cause) { + self.emit("error", cause instanceof RedirectionError ? + cause : new RedirectionError({ cause: cause })); + } }; // Perform the first request @@ -280,8 +287,7 @@ RedirectableRequest.prototype._performRequest = function () { var protocol = this._options.protocol; var nativeProtocol = this._options.nativeProtocols[protocol]; if (!nativeProtocol) { - this.emit("error", new TypeError("Unsupported protocol " + protocol)); - return; + throw new TypeError("Unsupported protocol " + protocol); } // If specified, use the agent corresponding to the protocol @@ -380,8 +386,7 @@ RedirectableRequest.prototype._processResponse = function (response) { // RFC7231ยง6.4: A client SHOULD detect and intervene // in cyclical redirections (i.e., "infinite" redirection loops). if (++this._redirectCount > this._options.maxRedirects) { - this.emit("error", new TooManyRedirectsError()); - return; + throw new TooManyRedirectsError(); } // Store the request headers if applicable @@ -421,14 +426,7 @@ RedirectableRequest.prototype._processResponse = function (response) { url.format(Object.assign(currentUrlParts, { host: currentHost })); // Determine the URL of the redirection - var redirectUrl; - try { - redirectUrl = url.resolve(currentUrl, location); - } - catch (cause) { - this.emit("error", new RedirectionError({ cause: cause })); - return; - } + var redirectUrl = url.resolve(currentUrl, location); // Create the redirected request debug("redirecting to", redirectUrl); @@ -456,23 +454,12 @@ RedirectableRequest.prototype._processResponse = function (response) { method: method, headers: requestHeaders, }; - try { - beforeRedirect(this._options, responseDetails, requestDetails); - } - catch (err) { - this.emit("error", err); - return; - } + beforeRedirect(this._options, responseDetails, requestDetails); this._sanitizeOptions(this._options); } // Perform the redirected request - try { - this._performRequest(); - } - catch (cause) { - this.emit("error", new RedirectionError({ cause: cause })); - } + this._performRequest(); }; // Wraps the key/value object of protocols with redirect functionality diff --git a/test/test.js b/test/test.js index aa62537..6aed484 100644 --- a/test/test.js +++ b/test/test.js @@ -984,8 +984,12 @@ describe("follow-redirects", function () { })) .catch(function (error) { assert(error instanceof Error); - assert(error instanceof TypeError); - assert.equal(error.message, "Unsupported protocol about:"); + assert.equal(error.message, "Redirected request failed: Unsupported protocol about:"); + + var cause = error.cause; + assert(cause instanceof Error); + assert(cause instanceof TypeError); + assert.equal(cause.message, "Unsupported protocol about:"); }); }); }); @@ -2153,7 +2157,9 @@ describe("follow-redirects", function () { .catch(function (error) { assert(!redirected); assert(error instanceof Error); - assert.equal(error.message, "no redirects!"); + assert.equal(error.message, "Redirected request failed: no redirects!"); + assert(error.cause instanceof Error); + assert.equal(error.cause.message, "no redirects!"); }); }); From 1cba8e85fa73f563a439fe460cf028688e4358df Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Sat, 30 Dec 2023 10:43:23 +0100 Subject: [PATCH 4/7] Prefer native URL instead of legacy url.resolve. --- index.js | 17 +++++++++++++---- test/test.js | 19 +------------------ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/index.js b/index.js index 30fc38c..67416dc 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,9 @@ var Writable = require("stream").Writable; var assert = require("assert"); var debug = require("./debug"); +// Whether to use the native URL object or the legacy url module +var hasNativeURL = typeof URL !== "undefined"; + // Create handlers that pass events from native requests var events = ["abort", "aborted", "connect", "error", "socket", "timeout"]; var eventHandlers = Object.create(null); @@ -15,12 +18,12 @@ events.forEach(function (event) { }; }); +// Error types with codes var InvalidUrlError = createErrorType( "ERR_INVALID_URL", "Invalid URL", TypeError ); -// Error types with codes var RedirectionError = createErrorType( "ERR_FR_REDIRECTION_FAILURE", "Redirected request failed" @@ -426,7 +429,7 @@ RedirectableRequest.prototype._processResponse = function (response) { url.format(Object.assign(currentUrlParts, { host: currentHost })); // Determine the URL of the redirection - var redirectUrl = url.resolve(currentUrl, location); + var redirectUrl = resolveUrl(location, currentUrl); // Create the redirected request debug("redirecting to", redirectUrl); @@ -494,7 +497,7 @@ function wrap(protocols) { } input = parsed; } - else if (URL && (input instanceof URL)) { + else if (hasNativeURL && (input instanceof URL)) { input = urlToOptions(input); } else { @@ -538,9 +541,15 @@ function wrap(protocols) { return exports; } -/* istanbul ignore next */ function noop() { /* empty */ } +function resolveUrl(relative, base) { + return !hasNativeURL ? + /* istanbul ignore next */ + url.resolve(base, relative) : + (new URL(relative, base)).href; +} + // from https://github.com/nodejs/node/blob/master/lib/internal/url.js function urlToOptions(urlObject) { var options = { diff --git a/test/test.js b/test/test.js index 6aed484..e35c0ad 100644 --- a/test/test.js +++ b/test/test.js @@ -364,7 +364,7 @@ describe("follow-redirects", function () { switch (error.cause.code) { // Node 17+ case "ERR_INVALID_URL": - assert.equal(error.message, "Redirected request failed: Invalid URL"); + assert.match(error.message, /^Redirected request failed: Invalid URL/); break; // Older Node versions case "ERR_UNESCAPED_CHARACTERS": @@ -376,23 +376,6 @@ describe("follow-redirects", function () { }); }); - it("emits an error when url.resolve fails", function () { - app.get("/a", redirectsTo("/b")); - var urlResolve = url.resolve; - url.resolve = function () { - throw new Error(); - }; - - return server.start(app) - .then(asPromise(function (resolve) { - http.get("http://localhost:3600/a").on("error", resolve); - })) - .then(function (error) { - url.resolve = urlResolve; - assert.equal(error.code, "ERR_FR_REDIRECTION_FAILURE"); - }); - }); - it("emits an error when the request fails for another reason", function () { app.get("/a", function (req, res) { res.socket.write("HTTP/1.1 301 Moved Permanently\r\n"); From 05629af696588b90d64e738bc2e809a97a5f92fc Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Sat, 30 Dec 2023 15:37:35 +0100 Subject: [PATCH 5/7] Prefer native URL instead of deprecated url.parse. --- index.js | 82 ++++++++++++++++++++++++++++++---------------------- test/test.js | 2 +- 2 files changed, 49 insertions(+), 35 deletions(-) diff --git a/index.js b/index.js index 67416dc..a1d18a9 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,20 @@ var debug = require("./debug"); // Whether to use the native URL object or the legacy url module var hasNativeURL = typeof URL !== "undefined"; +// URL fields to preserve in copy operations +var preservedUrlFields = [ + "auth", + "host", + "hostname", + "href", + "path", + "pathname", + "port", + "protocol", + "query", + "search", +]; + // Create handlers that pass events from native requests var events = ["abort", "aborted", "connect", "error", "socket", "timeout"]; var eventHandlers = Object.create(null); @@ -423,26 +437,23 @@ RedirectableRequest.prototype._processResponse = function (response) { var currentHostHeader = removeMatchingHeaders(/^host$/i, this._options.headers); // If the redirect is relative, carry over the host of the last request - var currentUrlParts = url.parse(this._currentUrl); + var currentUrlParts = parseUrl(this._currentUrl); var currentHost = currentHostHeader || currentUrlParts.host; var currentUrl = /^\w+:/.test(location) ? this._currentUrl : url.format(Object.assign(currentUrlParts, { host: currentHost })); - // Determine the URL of the redirection - var redirectUrl = resolveUrl(location, currentUrl); - // Create the redirected request - debug("redirecting to", redirectUrl); + var redirectUrl = resolveUrl(location, currentUrl); + debug("redirecting to", redirectUrl.href); this._isRedirect = true; - var redirectUrlParts = url.parse(redirectUrl); - Object.assign(this._options, redirectUrlParts); + spreadUrlObject(redirectUrl, this._options); // Drop confidential headers when redirecting to a less secure protocol // or to a different domain that is not a superdomain - if (redirectUrlParts.protocol !== currentUrlParts.protocol && - redirectUrlParts.protocol !== "https:" || - redirectUrlParts.host !== currentHost && - !isSubdomain(redirectUrlParts.host, currentHost)) { + if (redirectUrl.protocol !== currentUrlParts.protocol && + redirectUrl.protocol !== "https:" || + redirectUrl.host !== currentHost && + !isSubdomain(redirectUrl.host, currentHost)) { removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers); } @@ -486,7 +497,7 @@ function wrap(protocols) { if (isString(input)) { var parsed; try { - parsed = urlToOptions(new URL(input)); + parsed = spreadUrlObject(new URL(input)); } catch (err) { /* istanbul ignore next */ @@ -498,7 +509,7 @@ function wrap(protocols) { input = parsed; } else if (hasNativeURL && (input instanceof URL)) { - input = urlToOptions(input); + input = spreadUrlObject(input); } else { callback = options; @@ -543,31 +554,34 @@ function wrap(protocols) { function noop() { /* empty */ } +function parseUrl(string) { + /* istanbul ignore next */ + return hasNativeURL ? new URL(string) : url.parse(string); +} + function resolveUrl(relative, base) { - return !hasNativeURL ? - /* istanbul ignore next */ - url.resolve(base, relative) : - (new URL(relative, base)).href; + /* istanbul ignore next */ + return hasNativeURL ? new URL(relative, base) : parseUrl(url.resolve(base, relative)); } -// from https://github.com/nodejs/node/blob/master/lib/internal/url.js -function urlToOptions(urlObject) { - var options = { - protocol: urlObject.protocol, - hostname: urlObject.hostname.startsWith("[") ? - /* istanbul ignore next */ - urlObject.hostname.slice(1, -1) : - urlObject.hostname, - hash: urlObject.hash, - search: urlObject.search, - pathname: urlObject.pathname, - path: urlObject.pathname + urlObject.search, - href: urlObject.href, - }; - if (urlObject.port !== "") { - options.port = Number(urlObject.port); +function spreadUrlObject(urlObject, target) { + var spread = target || {}; + for (var key of preservedUrlFields) { + spread[key] = urlObject[key]; + } + + // Fix IPv6 hostname + if (spread.hostname.startsWith("[")) { + spread.hostname = spread.hostname.slice(1, -1); } - return options; + // Ensure port is a number + if (spread.port !== "") { + spread.port = Number(spread.port); + } + // Concatenate path + spread.path = spread.search ? spread.pathname + spread.search : spread.pathname; + + return spread; } function removeMatchingHeaders(regex, headers) { diff --git a/test/test.js b/test/test.js index e35c0ad..406b602 100644 --- a/test/test.js +++ b/test/test.js @@ -364,7 +364,7 @@ describe("follow-redirects", function () { switch (error.cause.code) { // Node 17+ case "ERR_INVALID_URL": - assert.match(error.message, /^Redirected request failed: Invalid URL/); + assert(/^Redirected request failed: Invalid URL/.test(error.message)); break; // Older Node versions case "ERR_UNESCAPED_CHARACTERS": From 7a6567e16dfa9ad18a70bfe91784c28653fbf19d Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Sat, 30 Dec 2023 16:15:57 +0100 Subject: [PATCH 6/7] Disallow bracketed hostnames. Fixes https://github.com/follow-redirects/follow-redirects/issues/235 --- index.js | 64 +++++++++++++++++---------- test/test.js | 120 ++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 151 insertions(+), 33 deletions(-) diff --git a/index.js b/index.js index a1d18a9..3232a72 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,13 @@ var assert = require("assert"); var debug = require("./debug"); // Whether to use the native URL object or the legacy url module -var hasNativeURL = typeof URL !== "undefined"; +var useNativeURL = false; +try { + assert(new URL()); +} +catch (error) { + useNativeURL = error.code === "ERR_INVALID_URL"; +} // URL fields to preserve in copy operations var preservedUrlFields = [ @@ -493,27 +499,16 @@ function wrap(protocols) { // Executes a request, following redirects function request(input, options, callback) { - // Parse parameters - if (isString(input)) { - var parsed; - try { - parsed = spreadUrlObject(new URL(input)); - } - catch (err) { - /* istanbul ignore next */ - parsed = url.parse(input); - } - if (!isString(parsed.protocol)) { - throw new InvalidUrlError({ input }); - } - input = parsed; - } - else if (hasNativeURL && (input instanceof URL)) { + // Parse parameters, ensuring that input is an object + if (isURL(input)) { input = spreadUrlObject(input); } + else if (isString(input)) { + input = spreadUrlObject(parseUrl(input)); + } else { callback = options; - options = input; + options = validateUrl(input); input = { protocol: protocol }; } if (isFunction(options)) { @@ -554,14 +549,35 @@ function wrap(protocols) { function noop() { /* empty */ } -function parseUrl(string) { - /* istanbul ignore next */ - return hasNativeURL ? new URL(string) : url.parse(string); +function parseUrl(input) { + var parsed; + /* istanbul ignore else */ + if (useNativeURL) { + parsed = new URL(input); + } + else { + // Ensure the URL is valid and absolute + parsed = validateUrl(url.parse(input)); + if (!isString(parsed.protocol)) { + throw new InvalidUrlError({ input }); + } + } + return parsed; } function resolveUrl(relative, base) { /* istanbul ignore next */ - return hasNativeURL ? new URL(relative, base) : parseUrl(url.resolve(base, relative)); + return useNativeURL ? new URL(relative, base) : parseUrl(url.resolve(base, relative)); +} + +function validateUrl(input) { + if (/^\[/.test(input.hostname) && !/^\[[:0-9a-f]+\]$/i.test(input.hostname)) { + throw new InvalidUrlError({ input: input.href || input }); + } + if (/^\[/.test(input.host) && !/^\[[:0-9a-f]+\](:\d+)?$/i.test(input.host)) { + throw new InvalidUrlError({ input: input.href || input }); + } + return input; } function spreadUrlObject(urlObject, target) { @@ -646,6 +662,10 @@ function isBuffer(value) { return typeof value === "object" && ("length" in value); } +function isURL(value) { + return URL && value instanceof URL; +} + // Exports module.exports = wrap({ http: http, https: https }); module.exports.wrap = wrap; diff --git a/test/test.js b/test/test.js index 406b602..5e53695 100644 --- a/test/test.js +++ b/test/test.js @@ -213,6 +213,67 @@ describe("follow-redirects", function () { }); }); + it("http.get to bracketed IPv4 address", function () { + var error = null; + try { + http.get("http://[127.0.0.1]:3600/a"); + } + catch (err) { + error = err; + } + assert(error instanceof Error); + assert(error instanceof TypeError); + assert.equal(error.code, "ERR_INVALID_URL"); + assert.equal(error.input, "http://[127.0.0.1]:3600/a"); + }); + + it("http.get to bracketed IPv4 address specified as host", function () { + var error = null; + try { + http.get({ + host: "[127.0.0.1]:3600", + path: "/a", + }); + } + catch (err) { + error = err; + } + assert(error instanceof Error); + assert(error instanceof TypeError); + assert.equal(error.code, "ERR_INVALID_URL"); + }); + + it("http.get to bracketed IPv4 address specified as hostname", function () { + var error = null; + try { + http.get({ + hostname: "[127.0.0.1]", + port: 3600, + path: "/a", + }); + } + catch (err) { + error = err; + } + assert(error instanceof Error); + assert(error instanceof TypeError); + assert.equal(error.code, "ERR_INVALID_URL"); + }); + + it("http.get to bracketed hostname", function () { + var error = null; + try { + http.get("http://[localhost]:3600/a"); + } + catch (err) { + error = err; + } + assert(error instanceof Error); + assert(error instanceof TypeError); + assert.equal(error.code, "ERR_INVALID_URL"); + assert.equal(error.input, "http://[localhost]:3600/a"); + }); + it("http.get redirecting to IPv4 address", function () { app.get("/a", redirectsTo("http://127.0.0.1:3600/b")); app.get("/b", sendsJson({ a: "b" })); @@ -241,6 +302,46 @@ describe("follow-redirects", function () { }); }); + it("http.get redirecting to bracketed IPv4 address", function () { + app.get("/a", redirectsTo("http://[127.0.0.1]:3600/b")); + app.get("/b", sendsJson({ a: "b" })); + + return server.start(app) + .then(asPromise(function (resolve, reject) { + http.get("http://localhost:3600/a", concatJson(reject)).on("error", resolve); + })) + .then(function (error) { + assert(error instanceof Error); + assert.equal(error.code, "ERR_FR_REDIRECTION_FAILURE"); + + var cause = error.cause; + assert(cause instanceof Error); + assert(cause instanceof TypeError); + assert.equal(cause.code, "ERR_INVALID_URL"); + assert.equal(cause.input, "http://[127.0.0.1]:3600/b"); + }); + }); + + it("http.get redirecting to bracketed hostname", function () { + app.get("/a", redirectsTo("http://[localhost]:3600/b")); + app.get("/b", sendsJson({ a: "b" })); + + return server.start(app) + .then(asPromise(function (resolve, reject) { + http.get("http://localhost:3600/a", concatJson(reject)).on("error", resolve); + })) + .then(function (error) { + assert(error instanceof Error); + assert.equal(error.code, "ERR_FR_REDIRECTION_FAILURE"); + + var cause = error.cause; + assert(cause instanceof Error); + assert(cause instanceof TypeError); + assert.equal(cause.code, "ERR_INVALID_URL"); + assert.equal(cause.input, "http://[localhost]:3600/b"); + }); + }); + it("http.get with response event", function () { app.get("/a", redirectsTo("/b")); app.get("/b", redirectsTo("/c")); @@ -266,8 +367,8 @@ describe("follow-redirects", function () { try { http.get("/relative"); } - catch (e) { - error = e; + catch (err) { + error = err; } assert(error instanceof Error); assert(error instanceof TypeError); @@ -963,9 +1064,9 @@ describe("follow-redirects", function () { .then(asPromise(function (resolve, reject) { http.get("http://localhost:3600/a") .on("response", function () { return reject(new Error("unexpected response")); }) - .on("error", reject); + .on("error", resolve); })) - .catch(function (error) { + .then(function (error) { assert(error instanceof Error); assert.equal(error.message, "Redirected request failed: Unsupported protocol about:"); @@ -1266,8 +1367,8 @@ describe("follow-redirects", function () { try { req.write(12345678); } - catch (e) { - error = e; + catch (err) { + error = err; } req.destroy(); assert(error instanceof Error); @@ -2132,12 +2233,9 @@ describe("follow-redirects", function () { throw new Error("no redirects!"); }, }; - http.get(options, concatJson(resolve, reject)).on("error", reject); + http.get(options, concatJson(reject)).on("error", resolve); })) - .then(function () { - assert.fail("request chain should have been aborted"); - }) - .catch(function (error) { + .then(function (error) { assert(!redirected); assert(error instanceof Error); assert.equal(error.message, "Redirected request failed: no redirects!"); From 65858205e59f1e23c9bf173348a7a7cbb8ac47f5 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Sat, 30 Dec 2023 18:26:51 +0100 Subject: [PATCH 7/7] Release version 1.15.4 of the npm package. --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b5fce58..9f47d97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "follow-redirects", - "version": "1.15.3", + "version": "1.15.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "follow-redirects", - "version": "1.15.3", + "version": "1.15.4", "funding": [ { "type": "individual", diff --git a/package.json b/package.json index eb90372..f32466d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "follow-redirects", - "version": "1.15.3", + "version": "1.15.4", "description": "HTTP and HTTPS modules that follow redirects.", "license": "MIT", "main": "index.js",