From ff1223c9e6f547c97a318bdbbde753d255c56a20 Mon Sep 17 00:00:00 2001 From: Cristian Klein Date: Wed, 19 Nov 2025 16:09:31 +0100 Subject: [PATCH 1/5] Add editorconfig --- .editorconfig | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..db2a145a1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 From 1af2d017f4c3e91a077335cefb6314c60146798b Mon Sep 17 00:00:00 2001 From: Cristian Klein Date: Wed, 19 Nov 2025 16:10:03 +0100 Subject: [PATCH 2/5] Add redirect_with_bad_redirector test --- test/redirect_with_bad_redirector.js | 52 ++++++++++++++++++++++++++++ test/utils/bad-redirector.js | 34 ++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 test/redirect_with_bad_redirector.js create mode 100644 test/utils/bad-redirector.js diff --git a/test/redirect_with_bad_redirector.js b/test/redirect_with_bad_redirector.js new file mode 100644 index 000000000..3ae64bb8a --- /dev/null +++ b/test/redirect_with_bad_redirector.js @@ -0,0 +1,52 @@ +var helpers = require('./helpers'), + should = require('should'), + sinon = require('sinon'), + http = require('http'), + needle = require('./../'); + +const port = 1234; + +describe('redirects with bad redirector', function() { + + var spies = {}, + servers = {}; + + before(function(done) { + servers.http = http.createServer(function(req, res) { + if (req.url == '/foo/bar') { + res.end('Redirected successfully!'); + return; + } + + /* Don't judge me. I found at least one server on the + * Internet doing this and it triggered a bug. */ + const body = 'Let me send you a body, although you only asked for a HEAD.'; + + const headers = + 'HTTP/1.1 302 Found\r\n' + + 'Connection: close\r\n' + + 'Location: /foo/bar\r\n' + + `Content-Length: ${Buffer.byteLength(body)}\r\n` + + '\r\n'; + + res.socket.write(headers + body); + res.socket.destroy(); + }).listen(port, done); + }) + + after(function(done) { + servers.http.close(done); + }) + + it('calls back exactly once', function (done) { + const opts = { + follow: 5, + } + + const url = `http://localhost:${port}` + needle.head(url, opts, function (err, resp, body) { + //should(body && body.toString()).eql('Redirected successfully!'); + done(); + }); + }); +}); diff --git a/test/utils/bad-redirector.js b/test/utils/bad-redirector.js new file mode 100644 index 000000000..92301ccfe --- /dev/null +++ b/test/utils/bad-redirector.js @@ -0,0 +1,34 @@ +var http = require('http'), + https = require('https'), + url = require('url'); + +var port = 1234, + log = true, + request_auth = false; + +http.createServer(function(req, res) { + + console.log(req.headers); + console.log("Got request: " + req.method + " " + req.url); + + /* Don't judge me. I found at least one server on the + * Internet doing this and it triggered a bug. */ + const body = 'Let me send you a body, although you only asked for a HEAD.'; + + const headers = + 'HTTP/1.1 302 Found\r\n' + + 'Connection: close\r\n' + + 'Location: /foo/bar\r\n' + + `Content-Length: ${Buffer.byteLength(body)}\r\n` + + '\r\n'; + + res.socket.write(headers + body); + res.socket.destroy(); +}).listen(port); + +process.on('uncaughtException', function(err){ + console.log('Uncaught exception!'); + console.log(err); +}); + +console.log("Bad redirector listening on port " + port); From e51e67bf0161f04687c268c848e5b57eb81cc7ad Mon Sep 17 00:00:00 2001 From: Cristian Klein Date: Wed, 19 Nov 2025 16:28:58 +0100 Subject: [PATCH 3/5] Add redirect test with follow_keep_method --- test/redirect_with_bad_redirector.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/redirect_with_bad_redirector.js b/test/redirect_with_bad_redirector.js index 3ae64bb8a..5116e7b72 100644 --- a/test/redirect_with_bad_redirector.js +++ b/test/redirect_with_bad_redirector.js @@ -49,4 +49,17 @@ describe('redirects with bad redirector', function() { done(); }); }); + + it('calls back exactly once with follow_keep_method', function (done) { + const opts = { + follow: 5, + follow_keep_method: true, + } + + const url = `http://localhost:${port}` + needle.head(url, opts, function (err, resp, body) { + //should(body && body.toString()).eql('Redirected successfully!'); + done(); + }); + }); }); From 4f3771bae01d07cb9ecb7884606333e306b27bb2 Mon Sep 17 00:00:00 2001 From: Cristian Klein Date: Wed, 19 Nov 2025 16:29:07 +0100 Subject: [PATCH 4/5] Avoid calling back with errors after redirect Fixes #338 --- lib/needle.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/needle.js b/lib/needle.js index e153b92eb..f459c2940 100644 --- a/lib/needle.js +++ b/lib/needle.js @@ -455,13 +455,8 @@ Needle.prototype.send_request = function(count, method, uri, config, post_data, protocol = request_opts.protocol == 'https:' ? https : http, signal = request_opts.signal; - function done(err, resp) { - if (returned++ > 0) - return debug('Already finished, stopping here.'); - - if (timer) clearTimeout(timer); + function no_more_errors_please() { request.removeListener('error', had_error); - out.done = true; // An error can still be fired after closing. In particular, on macOS. // See also: @@ -469,6 +464,16 @@ Needle.prototype.send_request = function(count, method, uri, config, post_data, // - https://github.com/less/less.js/issues/3693 // - https://github.com/nodejs/node/issues/27916 request.once('error', function() {}); + } + + function done(err, resp) { + if (returned++ > 0) + return debug('Already finished, stopping here.'); + + if (timer) clearTimeout(timer); + out.done = true; + + no_more_errors_please(); if (callback) return callback(err, resp, resp ? resp.body : undefined); @@ -559,6 +564,7 @@ Needle.prototype.send_request = function(count, method, uri, config, post_data, var redirect_url = utils.resolve_url(headers.location, uri); debug('Redirecting to ' + redirect_url.toString()); + no_more_errors_please(); return self.send_request(++count, method, redirect_url.toString(), config, post_data, out, callback); } else if (config.follow_max > 0) { return done(new Error('Max redirects reached. Possible loop in: ' + headers.location)); From a52baad790cf6174bc3aa0349425bd4fcb8677e6 Mon Sep 17 00:00:00 2001 From: Cristian Klein Date: Wed, 19 Nov 2025 18:36:22 +0100 Subject: [PATCH 5/5] Rename to unlisten_errors --- lib/needle.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/needle.js b/lib/needle.js index f459c2940..9a116a4d7 100644 --- a/lib/needle.js +++ b/lib/needle.js @@ -455,7 +455,7 @@ Needle.prototype.send_request = function(count, method, uri, config, post_data, protocol = request_opts.protocol == 'https:' ? https : http, signal = request_opts.signal; - function no_more_errors_please() { + function unlisten_errors() { request.removeListener('error', had_error); // An error can still be fired after closing. In particular, on macOS. @@ -473,7 +473,7 @@ Needle.prototype.send_request = function(count, method, uri, config, post_data, if (timer) clearTimeout(timer); out.done = true; - no_more_errors_please(); + unlisten_errors(); if (callback) return callback(err, resp, resp ? resp.body : undefined); @@ -564,7 +564,7 @@ Needle.prototype.send_request = function(count, method, uri, config, post_data, var redirect_url = utils.resolve_url(headers.location, uri); debug('Redirecting to ' + redirect_url.toString()); - no_more_errors_please(); + unlisten_errors(); return self.send_request(++count, method, redirect_url.toString(), config, post_data, out, callback); } else if (config.follow_max > 0) { return done(new Error('Max redirects reached. Possible loop in: ' + headers.location));