Comparing version 3.2.0 to 3.3.0
@@ -24,2 +24,3 @@ var iconv, | ||
var matches = regex.exec(chunk.toString()); | ||
if (matches) { | ||
@@ -32,3 +33,4 @@ var found = matches[1].toLowerCase().replace('utf8', 'utf-8'); // canonicalize; | ||
if (this.charset == 'utf-8') { // no need to decode, just pass through | ||
// if charset is already utf-8 or given encoding isn't supported, just pass through | ||
if (this.charset == 'utf-8' || !iconv.encodingExists(this.charset)) { | ||
this.push(chunk); | ||
@@ -35,0 +37,0 @@ return done(); |
////////////////////////////////////////// | ||
// Needle -- HTTP Client for Node.js | ||
// Written by Tomás Pollak <tomas@forkhq.com> | ||
// (c) 2012-2020 - Fork Ltd. | ||
// (c) 2012-2023 - Fork Ltd. | ||
// MIT Licensed | ||
@@ -13,3 +13,3 @@ ////////////////////////////////////////// | ||
stream = require('stream'), | ||
debug = require('debug')('needle'), | ||
debug = require('util').debuglog('needle'), | ||
stringify = require('./querystring').build, | ||
@@ -101,2 +101,5 @@ multipart = require('./multipart'), | ||
// abort signal | ||
signal : null, | ||
// booleans | ||
@@ -111,3 +114,4 @@ compressed : false, | ||
follow_if_same_protocol : false, | ||
follow_if_same_location : false | ||
follow_if_same_location : false, | ||
use_proxy_from_env_var : true | ||
} | ||
@@ -194,3 +198,4 @@ | ||
localAddress: get_option('localAddress', undefined), | ||
lookup: get_option('lookup', undefined) | ||
lookup: get_option('lookup', undefined), | ||
signal: get_option('signal', defaults.signal) | ||
}, // passed later to http.request() directly | ||
@@ -212,2 +217,5 @@ headers : {}, | ||
if (config.http_opts.signal && !(config.http_opts.signal instanceof AbortSignal)) | ||
throw new TypeError(typeof config.http_opts.signal + ' received for signal, but expected an AbortSignal'); | ||
// populate http_opts with given TLS options | ||
@@ -263,8 +271,10 @@ tls_options.split(' ').forEach(function(key) { | ||
var env_proxy = utils.get_env_var(['HTTP_PROXY', 'HTTPS_PROXY'], true); | ||
if (!config.proxy && env_proxy) config.proxy = env_proxy; | ||
if (config.use_proxy_from_env_var) { | ||
var env_proxy = utils.get_env_var(['HTTP_PROXY', 'HTTPS_PROXY'], true); | ||
if (!config.proxy && env_proxy) config.proxy = env_proxy; | ||
} | ||
// if proxy is present, set auth header from either url or proxy_user option. | ||
if (config.proxy) { | ||
if (utils.should_proxy_to(uri)) { | ||
if (!config.use_proxy_from_env_var || utils.should_proxy_to(uri)) { | ||
if (config.proxy.indexOf('http') === -1) | ||
@@ -452,2 +462,3 @@ config.proxy = 'http://' + config.proxy; | ||
protocol = request_opts.protocol == 'https:' ? https : http; | ||
signal = request_opts.signal; | ||
@@ -488,2 +499,7 @@ function done(err, resp) { | ||
function abort_handler() { | ||
out.emit('err', new Error('Aborted by signal.')); | ||
request.destroy(); | ||
} | ||
function set_timeout(type, milisecs) { | ||
@@ -495,9 +511,10 @@ if (timer) clearTimeout(timer); | ||
out.emit('timeout', type); | ||
request.abort(); | ||
request.destroy(); | ||
// also invoke done() to terminate job on read_timeout | ||
if (type == 'read') done(new Error(type + ' timeout')); | ||
signal && signal.removeEventListener('abort', abort_handler); | ||
}, milisecs); | ||
} | ||
debug('Making request #' + count, request_opts); | ||
@@ -775,3 +792,11 @@ request = protocol.request(request_opts, function(resp) { | ||
out.abort = function() { request.abort() }; // easier access | ||
if (signal) { // abort signal given, so handle it | ||
if (signal.aborted === true) { | ||
abort_handler(); | ||
} else { | ||
signal.addEventListener('abort', abort_handler, { once: true }); | ||
} | ||
} | ||
out.abort = function() { request.destroy() }; // easier access | ||
out.request = request; | ||
@@ -811,4 +836,4 @@ return out; | ||
if (defaults.hasOwnProperty(target_key) && typeof obj[key] != 'undefined') { | ||
if (target_key != 'parse_response' && target_key != 'proxy' && target_key != 'agent') { | ||
// ensure type matches the original, except for proxy/parse_response that can be null/bool or string | ||
if (target_key != 'parse_response' && target_key != 'proxy' && target_key != 'agent' && target_key != 'signal') { | ||
// ensure type matches the original, except for proxy/parse_response that can be null/bool or string, and signal that can be null/AbortSignal | ||
var valid_type = defaults[target_key].constructor.name; | ||
@@ -818,2 +843,4 @@ | ||
throw new TypeError('Invalid type for ' + key + ', should be ' + valid_type); | ||
} else if (target_key === 'signal' && obj[key] !== null && !(obj[key] instanceof AbortSignal)) { | ||
throw new TypeError('Invalid type for ' + key + ', should be AbortSignal'); | ||
} | ||
@@ -820,0 +847,0 @@ defaults[target_key] = obj[key]; |
@@ -100,2 +100,3 @@ ////////////////////////////////////////// | ||
'application/json', | ||
'application/hal+json', | ||
'text/javascript', | ||
@@ -102,0 +103,0 @@ 'application/vnd.api+json' |
{ | ||
"name": "needle", | ||
"version": "3.2.0", | ||
"version": "3.3.0", | ||
"description": "The leanest and most handsome HTTP client in the Nodelands.", | ||
@@ -43,3 +43,2 @@ "keywords": [ | ||
"dependencies": { | ||
"debug": "^3.2.6", | ||
"iconv-lite": "^0.6.3", | ||
@@ -46,0 +45,0 @@ "sax": "^1.2.4" |
@@ -61,2 +61,3 @@ Needle | ||
- Streaming non-UTF-8 charset decoding, via `iconv-lite` | ||
- Aborting any or all Needle requests using `AbortSignal` objects | ||
@@ -321,2 +322,3 @@ And yes, Mr. Wayne, it does come in black. | ||
- `uri_modifier`: Anonymous function taking request (or redirect location if following redirects) URI as an argument and modifying it given logic. It has to return a valid URI string for successful request. | ||
- `signal` : An `AbortSignal` object that can be used to abort any or all Needle requests. | ||
@@ -614,7 +616,15 @@ Response options | ||
Then you should be able to run `npm test` once you have the dependencies in place. | ||
To run the tests with debug logs, set the environment variable `NODE_DEBUG` to `needle` (for example, by running `NODE_DEBUG=needle npm test`). | ||
> Note: Tests currently only work on linux-based environments that have `/proc/self/fd`. They *do not* work on MacOS environments. | ||
> You can use Docker to run tests by creating a container and mounting the needle project directory on `/app` | ||
> `docker create --name Needle -v /app -w /app -v /app/node_modules -i node:argon` | ||
docker create --name Needle -v $(pwd) -w /app -v $(pwd)/node_modules -i node:argon | ||
Or alternatively: | ||
docker run -it -w /app --name Needle \ | ||
--mount type=bind,source="$(pwd)",target=/app \ | ||
node:fermium bash | ||
Credits | ||
@@ -621,0 +631,0 @@ ------- |
@@ -14,32 +14,27 @@ var should = require('should'), | ||
function staticServerFor(file, content_type) { | ||
return http.createServer(function(req, res) { | ||
req.on('data', function(chunk) {}) | ||
req.on('end', function() { | ||
// We used to pull from a particular site that is no longer up. | ||
// This is a local mirror pulled from archive.org | ||
// https://web.archive.org/web/20181003202907/http://www.nina.jp/server/slackware/webapp/tomcat_charset.html | ||
fs.readFile(file, function(err, data) { | ||
if (err) { | ||
res.writeHead(404); | ||
res.end(JSON.stringify(err)); | ||
return; | ||
} | ||
res.writeHeader(200, { 'Content-Type': content_type }) | ||
res.end(data); | ||
}); | ||
}) | ||
}) | ||
} | ||
describe('Given content-type: "text/html; charset=EUC-JP"', function() { | ||
var server, port = 2233; | ||
var port = 2233; | ||
var server; | ||
function createServer() { | ||
return http.createServer(function(req, res) { | ||
req.on('data', function(chunk) {}) | ||
req.on('end', function() { | ||
// We used to pull from a particular site that is no longer up. | ||
// This is a local mirror pulled from archive.org | ||
// https://web.archive.org/web/20181003202907/http://www.nina.jp/server/slackware/webapp/tomcat_charset.html | ||
fs.readFile('test/tomcat_charset.html', function(err, data) { | ||
if (err) { | ||
res.writeHead(404); | ||
res.end(JSON.stringify(err)); | ||
return; | ||
} | ||
res.writeHeader(200, { 'Content-Type': 'text/html; charset=EUC-JP' }) | ||
res.end(data); | ||
}); | ||
}) | ||
}) | ||
} | ||
before(function(done) { | ||
server = createServer(); | ||
server = staticServerFor('test/files/tomcat_charset.html', 'text/html; charset=EUC-JP') | ||
server.listen(port, done) | ||
@@ -54,5 +49,3 @@ url = 'http://localhost:' + port; | ||
describe('with decode = false', function() { | ||
it('does not decode', function(done) { | ||
needle.get(url, { decode: false }, function(err, resp) { | ||
@@ -64,11 +57,7 @@ resp.body.should.be.a.String; | ||
}) | ||
}) | ||
}) | ||
describe('with decode = true', function() { | ||
it('decodes', function(done) { | ||
needle.get(url, { decode: true }, function(err, resp) { | ||
@@ -80,7 +69,4 @@ resp.body.should.be.a.String; | ||
}) | ||
}) | ||
}) | ||
}) | ||
@@ -126,2 +112,39 @@ | ||
describe('Given content-type: text/html; charset=maccentraleurope', function() { | ||
var server, port = 2233; | ||
// from 'https://wayback.archive-it.org/3259/20160921140616/https://www.arc.gov/research/MapsofAppalachia.asp?MAP_ID=11'; | ||
before(function(done) { | ||
server = staticServerFor('test/files/Appalachia.html', 'text/html; charset=maccentraleurope') | ||
server.listen(port, done) | ||
url = 'http://localhost:' + port; | ||
}) | ||
after(function(done) { | ||
server.close(done) | ||
}) | ||
describe('with decode = false', function() { | ||
it('does not decode', function(done) { | ||
needle.get(url, { decode: false }, function(err, resp) { | ||
resp.body.should.be.a.String; | ||
chardet.detect(resp.body).encoding.should.eql('ascii'); | ||
done(); | ||
}) | ||
}) | ||
}) | ||
describe('with decode = true', function() { | ||
it('does not explode', function(done) { | ||
(function() { | ||
needle.get(url, { decode: true }, function(err, resp) { | ||
resp.body.should.be.a.String; | ||
chardet.detect(resp.body).encoding.should.eql('ascii'); | ||
done(); | ||
}) | ||
}).should.not.throw(); | ||
}) | ||
}) | ||
}) | ||
describe('Given content-type: "text/html"', function () { | ||
@@ -158,3 +181,2 @@ | ||
}) | ||
}) | ||
@@ -161,0 +183,0 @@ |
@@ -272,2 +272,136 @@ var needle = require('../'), | ||
var node_major_ver = process.version.split('.')[0].replace('v', ''); | ||
if (node_major_ver >= 16) { | ||
describe('when request is aborted by signal', function() { | ||
var server, | ||
url = 'http://localhost:3333/foo'; | ||
before(function() { | ||
server = helpers.server({ port: 3333, wait: 600 }); | ||
}) | ||
after(function() { | ||
server.close(); | ||
}) | ||
afterEach(function() { | ||
// reset signal to default | ||
needle.defaults({signal: null}); | ||
}) | ||
it('works if passing an already aborted signal aborts the request', function(done) { | ||
var abortedSignal = AbortSignal.abort(); | ||
var start = new Date(); | ||
abortedSignal.aborted.should.equal(true); | ||
needle.get(url, { signal: abortedSignal, response_timeout: 10000 }, function(err, res) { | ||
var timediff = (new Date() - start); | ||
should.not.exist(res); | ||
err.code.should.equal('ABORT_ERR'); | ||
timediff.should.be.within(0, 50); | ||
done(); | ||
}); | ||
}) | ||
it('works if request aborts before timing out', function(done) { | ||
var cancel = new AbortController(); | ||
var start = new Date(); | ||
needle.get(url, { signal: cancel.signal, response_timeout: 500, open_timeout: 500, read_timeout: 500 }, function(err, res) { | ||
var timediff = (new Date() - start); | ||
should.not.exist(res); | ||
if (node_major_ver <= 16) | ||
err.code.should.equal('ECONNRESET'); | ||
if (node_major_ver > 16) | ||
err.code.should.equal('ABORT_ERR'); | ||
cancel.signal.aborted.should.equal(true); | ||
timediff.should.be.within(200, 250); | ||
done(); | ||
}); | ||
function abort() { | ||
cancel.abort(); | ||
} | ||
setTimeout(abort, 200); | ||
}) | ||
it('works if request times out before being aborted', function(done) { | ||
var cancel = new AbortController(); | ||
var start = new Date(); | ||
needle.get(url, { signal: cancel.signal, response_timeout: 200, open_timeout: 200, read_timeout: 200 }, function(err, res) { | ||
var timediff = (new Date() - start); | ||
should.not.exist(res); | ||
err.code.should.equal('ECONNRESET'); | ||
timediff.should.be.within(200, 250); | ||
}); | ||
function abort() { | ||
cancel.signal.aborted.should.equal(false); | ||
done(); | ||
} | ||
setTimeout(abort, 500); | ||
}) | ||
it('works if setting default signal aborts all requests', function(done) { | ||
var cancel = new AbortController(); | ||
needle.defaults({signal: cancel.signal}); | ||
var start = new Date(); | ||
var count = 0; | ||
function cb(err, res) { | ||
var timediff = (new Date() - start); | ||
should.not.exist(res); | ||
if (node_major_ver <= 16) | ||
err.code.should.equal('ECONNRESET'); | ||
if (node_major_ver > 16) | ||
err.code.should.equal('ABORT_ERR'); | ||
cancel.signal.aborted.should.equal(true); | ||
timediff.should.be.within(200, 250); | ||
if ( count++ === 2 ) done(); | ||
} | ||
needle.get(url, { timeout: 300 }, cb); | ||
needle.get(url, { timeout: 350 }, cb); | ||
needle.get(url, { timeout: 400 }, cb); | ||
function abort() { | ||
cancel.abort(); | ||
} | ||
setTimeout(abort, 200); | ||
}) | ||
it('does not work if invalid signal passed', function(done) { | ||
try { | ||
needle.get(url, { signal: 'invalid signal' }, function(err, res) { | ||
done(new Error('A bad option error expected to be thrown')); | ||
}); | ||
} catch(e) { | ||
e.should.be.a.TypeError; | ||
done(); | ||
} | ||
}) | ||
it('does not work if invalid signal set by default', function(done) { | ||
try { | ||
needle.defaults({signal: new Error(), timeout: 1200}); | ||
done(new Error('A bad option error expected to be thrown')); | ||
} catch(e) { | ||
e.should.be.a.TypeError; | ||
done(); | ||
} | ||
}) | ||
}) | ||
} | ||
}) |
@@ -547,2 +547,43 @@ var should = require('should'), | ||
describe('when response is a HAL JSON content-type', function () { | ||
var json_string = '{"name": "Tomás", "_links": {"href": "https://github.com/tomas/needle.git"}}'; | ||
before(function(done){ | ||
server = http.createServer(function(req, res) { | ||
res.setHeader('Content-Type', 'application/hal+json'); | ||
res.end(json_string); | ||
}).listen(port, done); | ||
}); | ||
after(function(done){ | ||
server.close(done); | ||
}); | ||
describe('and parse option is not passed', function() { | ||
describe('with default parse_response', function() { | ||
before(function() { | ||
needle.defaults().parse_response.should.eql('all') | ||
}) | ||
it('should return object', function(done){ | ||
needle.get('localhost:' + port, function(err, response, body){ | ||
should.ifError(err); | ||
body.should.deepEqual({ | ||
'name': 'Tomás', | ||
'_links': { | ||
'href': 'https://github.com/tomas/needle.git' | ||
}}); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}) | ||
}); | ||
}) |
@@ -231,2 +231,39 @@ var helpers = require('./helpers'), | ||
describe('when environment variable is set', function() { | ||
describe('and default is unchanged', function() { | ||
before(function() { | ||
process.env.HTTP_PROXY = 'foobar'; | ||
}) | ||
after(function() { | ||
delete process.env.HTTP_PROXY; | ||
}) | ||
it('tries to proxy', function(done) { | ||
send_request({}, proxied('foobar', 80, done)) | ||
}) | ||
}) | ||
describe('and functionality is disabled', function() { | ||
before(function() { | ||
process.env.HTTP_PROXY = 'foobar'; | ||
}) | ||
after(function() { | ||
delete process.env.HTTP_PROXY; | ||
}) | ||
it('ignores proxy', function(done) { | ||
send_request({ | ||
use_proxy_from_env_var: false | ||
}, not_proxied(done)) | ||
}) | ||
}) | ||
}) | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
323689
2
57
6108
638
1
54
- Removeddebug@^3.2.6
- Removeddebug@3.2.7(transitive)
- Removedms@2.1.3(transitive)