diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..ac29b92 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,14 @@ +{ + "plugins": [ + "mocha" + ], + "extends": "@aptoma/eslint-config", + "parserOptions": { + "ecmaVersion": 9 + }, + "env": { + "node": true, + "mocha": true, + "es6": true + } +} diff --git a/.gitignore b/.gitignore index ba2a97b..1fd04da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules coverage +.nyc_output diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index b0cb9ee..0000000 --- a/.jshintrc +++ /dev/null @@ -1,43 +0,0 @@ -{ - "browser" : false, - "devel" : true, - "node" : true, - - "asi" : false, - "bitwise" : false, - "boss" : false, - "curly" : true, - "debug" : false, - "eqeqeq" : true, - "eqnull" : true, - "evil" : false, - "forin" : false, - "immed" : true, - "laxbreak" : false, - "maxerr" : 100, - "maxlen" : 0, - "newcap" : false, - "noarg" : true, - "noempty" : false, - "nonew" : false, - "nomen" : false, - "onevar" : false, - "passfail" : false, - "plusplus" : false, - "regexp" : false, - "undef" : false, - "unused" : "vars", - "sub" : true, - "strict" : true, - "globalstrict": true, - "white" : true, - "expr" : true, - "smarttabs": true, - "predef" : [ - "-Promise", - "describe", - "it", - "beforeEach", - "afterEach" - ] -} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 99bf4ea..cbbceb9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,5 @@ language: node_js node_js: - - 0.10 - - 0.12 - - 4 - - 5 - -script: - - npm run ci + - 6 + - 10 + - 12 diff --git a/index.js b/index.js index 9f9ab3a..78c48a4 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,10 @@ 'use strict'; -var request = require('request'); -var streamify = require('streamify'); -var path = require('path'); -var fs = require('fs'); -var Readable = require('stream').Readable; -var Promise = require('bluebird'); +const request = require('request'); +const streamify = require('streamify'); +const path = require('path'); +const fs = require('fs'); +const Readable = require('stream').Readable; +const Promise = require('bluebird'); /** * Is status code 2xx @@ -21,7 +21,7 @@ function isOk(statusCode) { * @return {Promise} resolves with response or rejects with ResponseError or ConnectionError */ function requestProm(opts) { - return new Promise(function (resolve, reject) { + return new Promise((resolve, reject) => { createRequest(opts, resolve, reject); }); } @@ -32,7 +32,7 @@ function requestProm(opts) { * @param {Object} [opts] options to request * @return {Promise} resolves with response */ -['GET', 'POST', 'PATCH', 'DELETE', 'HEAD', 'PUT'].forEach(function (method) { +['GET', 'POST', 'PATCH', 'DELETE', 'HEAD', 'PUT'].forEach((method) => { // create short hand functions requestProm[method.toLowerCase()] = function (url, opts) { opts = opts || {}; @@ -44,27 +44,26 @@ function requestProm(opts) { /** * Make a request that returns a stream thats not sensitive to use after a process.nextTick() - * @param {String} url * @param {Object} opts options to request * @return {Stream} from streamify */ - requestProm.stream = function (opts) { - var stream = streamify(); +requestProm.stream = function (opts) { + const stream = streamify(); opts = opts || {}; - var req = request(opts); - req.on('error', function (err) { + const req = request(opts); + req.on('error', (err) => { if (err.code && (err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT')) { return stream.emit('error', new ConnectionError( 'Connect timeout occurred when requesting url: ' + (opts.url || opts.uri), - err.code + err.code )); } return stream.emit('error', new ConnectionError(err.message, err.code)); }); - req.on('response', function (res) { + req.on('response', (res) => { if (!isOk(res.statusCode)) { stream.emit('error', new ResponseError( 'Server responded with ' + res.statusCode + ', unable get data from url: ' + (opts.url || opts.uri), @@ -84,19 +83,19 @@ function requestProm(opts) { * @param {Object} [opts] options to request * @return {Promise} resolves with response */ - requestProm.postFile = function (url, file, opts) { +requestProm.postFile = function (url, file, opts) { opts = opts || {}; opts.url = url; opts.method = 'POST'; - return new Promise(function (resolve, reject) { - var req = createRequest(opts, resolve, reject), - form = req.form(); + return new Promise((resolve, reject) => { + const req = createRequest(opts, resolve, reject); + const form = req.form(); if (file instanceof Readable) { form.append('file', file); } else { - form.append('file', fs.createReadStream(file), { filename: path.basename(file) }); + form.append('file', fs.createReadStream(file), {filename: path.basename(file)}); } }); }; @@ -109,38 +108,76 @@ function requestProm(opts) { * @return {Request} */ function createRequest(opts, resolve, reject) { - return request(opts, - function (err, res, body) { - if (err) { - if (err.code && (err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT')) { - return reject(new ConnectionError( - 'Connect timeout occurred when requesting url: ' + (opts.url || opts.uri), - err.code - )); - } - - return reject(new ConnectionError(err.message, err.code)); + if (opts.timeout && (opts.socketTimeout || opts.connectTimeout)) { + reject(new Error('Can\'t use socketTimeout/connectTimeout in conjuction with timeout')); + } + + const req = request(opts, (err, res, body) => { // eslint-disable-line complexity + if (err) { + if (err.code && (err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT')) { + return reject(new ConnectionError( + `Connect timeout occurred when requesting url: ${opts.url || opts.uri}`, + err.code + )); } - if (!isOk(res.statusCode)) { - return reject(new ResponseError('Request to ' + (opts.url || opts.uri) + ' failed. code: ' + res.statusCode, res)); - } + return reject(new ConnectionError(err.message, err.code)); + } - if (opts.json && typeof(body) !== 'object') { - return reject(new ResponseError('Unable to parse json from url: ' + (opts.url || opts.uri), res)); - } + if (!isOk(res.statusCode)) { + return reject(new ResponseError(`Request to ${opts.url || opts.uri} failed. code: ${res.statusCode}`, res)); + } - return resolve(res); + if (opts.json && typeof (body) !== 'object') { + return reject(new ResponseError(`Unable to parse json from url: ${opts.url || opts.uri}`, res)); } - ); + + return resolve(res); + }); + + if (opts.socketTimeout) { + req.on('socket', (socket) => { + if (!socket.connecting) { + socket.setTimeout(opts.socketTimeout, sockTimeout); + return; + } + + socket.on('connect', () => { + socket.setTimeout(opts.socketTimeout, sockTimeout); + }); + }); + } + + if (opts.connectTimeout) { + const connectTimeoutId = setTimeout(() => { + if (req.req) { + req.abort(); + const e = new Error('ESOCKETTIMEDOUT'); + e.code = 'ESOCKETTIMEDOUT'; + e.connect = true; + req.emit('error', e); + } + }, opts.connectTimeout); + + req.on('connect', () => { + clearTimeout(connectTimeoutId); + }); + } + + return req; + + function sockTimeout() { + req.abort(); + const e = new Error('ESOCKETTIMEDOUT'); + e.code = 'ESOCKETTIMEDOUT'; + e.connect = false; + req.emit('error', e); + } } -/** - * ResponseError - */ function ResponseError(message, response) { this.message = message; - this.name = "ResponseError"; + this.name = 'ResponseError'; this.response = response; this.statusCode = response.statusCode; Error.captureStackTrace(this, ResponseError); @@ -150,13 +187,10 @@ ResponseError.prototype.constructor = ResponseError; requestProm.ResponseError = ResponseError; -/** - * ConnectionError - */ function ConnectionError(message, code) { this.message = message; this.code = code; - this.name = "ConnectionError"; + this.name = 'ConnectionError'; Error.captureStackTrace(this, ConnectionError); } ConnectionError.prototype = Object.create(Error.prototype); diff --git a/package.json b/package.json index ac26d87..c713284 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "description": "Bluebird promise wrapper for request.", "main": "index.js", "scripts": { - "test": "jshint test/ index.js && istanbul test --preload-sources _mocha -- -u exports -R spec test/*.test.js test/**/*.test.js", - "ci": "npm test --coverage && istanbul report cobertura", + "lint": "eslint --ext '.js' .", + "test": "npm run lint && NODE_ENV=test nyc --reporter=text-summary --reporter=lcov mocha --exit 'test/*.test.js' 'test/**/*.test.js'", + "watch": "NODE_ENV=test BLUEBIRD_DEBUG=1 mocha --watch 'test/**/*.js' 'index.js' --timeout 1000", "release": "npm test && release-it -n -i patch", "release:minor": "npm test && release-it -n -i minor", "release:major": "npm test && release-it -n -i major" @@ -27,16 +28,18 @@ }, "homepage": "https://github.com/martinj/node-request-prom", "dependencies": { - "bluebird": "^3.4.6", - "request": "^2.75.0", + "bluebird": "^3.5.4", + "request": "^2.88.0", "streamify": "^0.2.6" }, "devDependencies": { - "istanbul": "^0.4.5", - "jshint": "^2.9.3", - "mocha": "^3.1.2", - "nock": "^8.1.0", + "@aptoma/eslint-config": "^7.0.1", + "eslint": "^5.16.0", + "eslint-plugin-mocha": "^5.3.0", + "mocha": "^6.1.4", + "nock": "^10.0.6", + "nyc": "^14.1.0", "release-it": "^2.4.3", - "should": "^11.1.1" + "should": "^13.2.3" } } diff --git a/test/index.test.js b/test/index.test.js index 98fb80a..0066064 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,14 +1,14 @@ 'use strict'; -var req = require('../'); -var ResponseError = req.ResponseError; -var ConnectionError = req.ConnectionError; -var nock = require('nock'); -var should = require('should'); +const req = require('../'); +const ResponseError = req.ResponseError; +const ConnectionError = req.ConnectionError; +const nock = require('nock'); +const should = require('should'); -describe('request-prom', function () { - var url = 'http://foo.com'; +describe('request-prom', () => { + const url = 'http://foo.com'; - beforeEach(function () { + beforeEach(() => { nock(url) .get('/500') .reply(500) @@ -33,18 +33,21 @@ describe('request-prom', function () { .reply(200) .get('/error') .replyWithError('shit') + .get('/slowConnect') + .delay({head: 1000}) + .reply(200) .post('/postFile') - .reply(200, function (path, body) { + .reply(200, (path, body) => { return body.match(/filename="index\.test\.js"/) ? 'OK' : 'FAIL'; }); }); - afterEach(function () { + afterEach(() => { nock.cleanAll(); }); - it('should reject with ResponseError on response failure', function (done) { - req({ url: url + '/500' }).catch(ResponseError, function (e) { + it('should reject with ResponseError on response failure', (done) => { + req({url: url + '/500'}).catch(ResponseError, (e) => { e.message.should.equal('Request to http://foo.com/500 failed. code: 500'); e.statusCode.should.equal(500); should.exists(e.response); @@ -52,8 +55,8 @@ describe('request-prom', function () { }).catch(done); }); - it('should should look for failed url in opts.uri', function (done) { - req({ uri: url + '/500' }).catch(ResponseError, function (e) { + it('should should look for failed url in opts.uri', (done) => { + req({uri: url + '/500'}).catch(ResponseError, (e) => { e.message.should.equal('Request to http://foo.com/500 failed. code: 500'); e.statusCode.should.equal(500); should.exists(e.response); @@ -61,94 +64,121 @@ describe('request-prom', function () { }).catch(done); }); - it('should reject with ConnectionError on timeout', function (done) { - req({ url: url + '/timeout', timeout: 10 }).catch(ConnectionError, function (e) { + it('should reject with ConnectionError on timeout', (done) => { + req({url: url + '/timeout', timeout: 10}).catch(ConnectionError, (e) => { e.code.should.equal('ESOCKETTIMEDOUT'); done(); }).catch(done); }); - it('should reject with ConnectionError on request error', function (done) { - req({ url: url + '/error' }).catch(ConnectionError, function (e) { + it('should reject with ConnectionError on request error', (done) => { + req({url: url + '/error'}).catch(ConnectionError, () => { done(); }).catch(done); }); - it('should validate json parsing', function (done) { - req({ url: url + '/badJSON', json: true }).catch(ResponseError, function (e) { + it('should validate json parsing', (done) => { + req({url: url + '/badJSON', json: true}).catch(ResponseError, (e) => { e.message.should.equal('Unable to parse json from url: http://foo.com/badJSON'); done(); }).catch(done); }); - describe('stream()', function () { - it('should return stream that works with nextTick', function (done) { - var stream = req.stream({ url: url + '/file' }); - var content = ''; - process.nextTick(function () { - stream.on('data', function (d) { + describe('stream()', () => { + it('should return stream that works with nextTick', (done) => { + const stream = req.stream({url: url + '/file'}); + let content = ''; + process.nextTick(() => { + stream.on('data', (d) => { content += d; }); - stream.on('end', function () { + stream.on('end', () => { content.should.equal('some content'); done(); }); }); }); - it('should emit error with ResponseError if response is not ok', function (done) { - var stream = req.stream({ url: url + '/500' }); - stream.on('error', function (err) { + it('should emit error with ResponseError if response is not ok', (done) => { + const stream = req.stream({url: url + '/500'}); + stream.on('error', (err) => { err.should.be.instanceOf(ResponseError); done(); }); }); - it('should emit error with ConnectionError on timeout', function (done) { - var stream = req.stream({ url: url + '/timeout', timeout: 10 }); - var firstError = true; - stream.on('error', function (err) { + it('should emit error with ConnectionError on timeout', (done) => { + const stream = req.stream({url: url + '/timeout', timeout: 10}); + stream.on('error', (err) => { err.should.be.instanceOf(ConnectionError); - if (firstError) { - firstError = false; - err.code.should.equal('ESOCKETTIMEDOUT'); - } else { - err.message.should.equal('socket hang up'); - done(); - } + err.code.should.equal('ESOCKETTIMEDOUT'); + done(); }); }); }); - describe('postFile()', function () { - it('should add filename if path is used', function () { - req.postFile(url + '/postFile', __dirname + '/index.test.js').then(function (res) { + describe('postFile()', () => { + it('should add filename if path is used', (done) => { + req.postFile(url + '/postFile', __dirname + '/index.test.js').then((res) => { res.body.should.equal('OK'); + done(); }); }); }); - describe('Short hand methods', function () { - ['get', 'post', 'head', 'delete', 'patch', 'put'].forEach(function (method) { - it('should make a ' + method.toUpperCase() + ' request', function (done) { - req[method](url + '/' + method).then(function (res) { + describe('Short hand methods', () => { + ['get', 'post', 'head', 'delete', 'patch', 'put'].forEach((method) => { + it('should make a ' + method.toUpperCase() + ' request', (done) => { + req[method](url + '/' + method).then((res) => { res.statusCode.should.equal(200); done(); }).catch(done); }); }); - it('should make a get request with custom header', function (done) { + it('should make a get request with custom header', (done) => { nock(url, {reqheaders: {'User-Agent': 'testo'}}) .get('/get/header') .reply(200); - req.get(url + '/get/header', { headers: {'User-Agent': 'testo'}}).then(function (res) { + req.get(url + '/get/header', {headers: {'User-Agent': 'testo'}}).then((res) => { res.statusCode.should.equal(200); done(); }).catch(done); }); }); + + describe('Additional timeouts', () => { + it('should reject if option timeout is used with socketTimeout or connectTimeout', (done) => { + req({url: url + '/foo', timeout: 100, socketTimeout: 100}) + .catch((err) => { + err.message.should.equal('Can\'t use socketTimeout/connectTimeout in conjuction with timeout'); + return req({url: url + '/foo', timeout: 100, connectTimeout: 100}); + }) + .catch((err) => { + err.message.should.equal('Can\'t use socketTimeout/connectTimeout in conjuction with timeout'); + done(); + }); + }); + + describe('connectTimeout', () => { + it('rejects on timeout with ConnectionError', (done) => { + req({url: url + '/slowConnect', connectTimeout: 10}) + .catch(ConnectionError, () => { + done(); + }); + }); + }); + + describe('socketTimeout', () => { + it('rejects on timeout with ConnectionError', (done) => { + req({url: url + '/timeout', socketTimeout: 10}) + .catch(ConnectionError, () => { + done(); + }); + }); + }); + }); });