diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..62562b74 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +coverage +node_modules diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..e3578aad --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "standard" +} diff --git a/.travis.yml b/.travis.yml index 0280df88..c5c1f197 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,15 +6,24 @@ node_js: - "1.8" - "2.5" - "3.3" - - "4.2" - - "5.4" + - "4.4" + - "5.11" + - "6.1" sudo: false +cache: + directories: + - node_modules before_install: # Setup Node.js version-specific dependencies - - "test $TRAVIS_NODE_VERSION != '0.8' || npm rm --save-dev istanbul" + - "test $TRAVIS_NODE_VERSION != '0.8' || npm rm --save-dev eslint eslint-config-standard eslint-plugin-promise eslint-plugin-standard istanbul" + + # Update Node.js modules + - "test ! -d node_modules || npm prune" + - "test ! -d node_modules || npm rebuild" script: # Run test script, depending on istanbul install - "test ! -z $(npm -ps ls istanbul) || npm test" - "test -z $(npm -ps ls istanbul) || npm run-script test-travis" + - "test -z $(npm -ps ls eslint ) || npm run-script lint" after_script: - "test -e ./coverage/lcov.info && npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" diff --git a/HISTORY.md b/HISTORY.md index 4f68cf9a..663f0de0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,16 @@ +1.6.2 / 2016-05-12 +================== + + * deps: accepts@~1.3.3 + - deps: mime-types@~2.1.11 + - deps: negotiator@0.6.1 + * deps: bytes@2.3.0 + - Drop partial bytes on all parsed units + - Fix parsing byte string that looks like hex + - perf: hoist regular expressions + * deps: compressible@~2.0.8 + - deps: mime-db@'>= 1.23.0 < 2' + 1.6.1 / 2016-01-19 ================== diff --git a/index.js b/index.js index 05deb305..e6c494ce 100644 --- a/index.js +++ b/index.js @@ -21,6 +21,8 @@ var debug = require('debug')('compression') var onHeaders = require('on-headers') var vary = require('vary') var zlib = require('zlib') +var OutgoingMessage = require('http').OutgoingMessage +var hasCallback = (OutgoingMessage.prototype.write.length === 3) /** * Module exports. @@ -39,12 +41,12 @@ var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/ /** * Compress response data with gzip / deflate. * - * @param {Object} options + * @param {Object} [options] * @return {Function} middleware * @public */ -function compression(options) { +function compression (options) { var opts = options || {} // options @@ -55,17 +57,19 @@ function compression(options) { threshold = 1024 } - return function compression(req, res, next){ + return function compression (req, res, next) { var ended = false var length var listeners = [] - var write = res.write - var on = res.on - var end = res.end var stream + var noop = function () {} + + res._end = res.end + res._write = res.write + var _on = res.on // flush - res.flush = function flush() { + res.flush = function flush () { if (stream) { stream.flush() } @@ -73,25 +77,29 @@ function compression(options) { // proxy - res.write = function(chunk, encoding){ + res.write = function write (chunk, encoding, cb) { if (ended) { return false } + cb = hasCallback ? cb : null + if (!this._header) { this._implicitHeader() } return stream - ? stream.write(new Buffer(chunk, encoding)) - : write.call(this, chunk, encoding) - }; + ? stream.write(new Buffer(chunk, encoding), cb) + : res._write.call(this, chunk, encoding, cb) + } - res.end = function(chunk, encoding){ + res.end = function end (chunk, encoding, cb) { if (ended) { return false } + cb = hasCallback ? cb : null + if (!this._header) { // estimate the length if (!this.getHeader('Content-Length')) { @@ -102,7 +110,7 @@ function compression(options) { } if (!stream) { - return end.call(this, chunk, encoding) + return res._end.call(this, chunk, encoding, cb) } // mark ended @@ -110,13 +118,13 @@ function compression(options) { // write Buffer for Node.js 0.8 return chunk - ? stream.end(new Buffer(chunk, encoding)) - : stream.end() - }; + ? stream.end(new Buffer(chunk, encoding), null, cb) + : stream.end(null, null, cb) + } - res.on = function(type, listener){ + res.on = function on (type, listener) { if (!listeners || type !== 'drain') { - return on.call(this, type, listener) + return _on.call(this, type, listener) } if (stream) { @@ -129,13 +137,13 @@ function compression(options) { return this } - function nocompress(msg) { + function nocompress (msg) { debug('no compression: %s', msg) - addListeners(res, on, listeners) + addListeners(res, _on, listeners) listeners = null } - onHeaders(res, function(){ + onHeaders(res, function onResponseHeaders () { // determine if request is filtered if (!filter(req, res)) { nocompress('filtered') @@ -157,16 +165,16 @@ function compression(options) { return } - var encoding = res.getHeader('Content-Encoding') || 'identity'; + var encoding = res.getHeader('Content-Encoding') || 'identity' // already encoded - if ('identity' !== encoding) { + if (encoding !== 'identity') { nocompress('already encoded') return } // head - if ('HEAD' === req.method) { + if (req.method === 'HEAD') { nocompress('HEAD request') return } @@ -196,27 +204,27 @@ function compression(options) { addListeners(stream, stream.on, listeners) // header fields - res.setHeader('Content-Encoding', method); - res.removeHeader('Content-Length'); + res.setHeader('Content-Encoding', method) + res.removeHeader('Content-Length') // compression - stream.on('data', function(chunk){ - if (write.call(res, chunk) === false) { + stream.on('data', function onStreamData (chunk) { + if (res._write.call(res, chunk) === false) { stream.pause() } - }); + }) - stream.on('end', function(){ - end.call(res); - }); + stream.on('end', function onStreamEnd () { + res._end.call(res) + }) - on.call(res, 'drain', function() { + _on.call(res, 'drain', function onResponseDrain () { stream.resume() - }); - }); + }) + }) - next(); - }; + next() + } } /** @@ -224,7 +232,7 @@ function compression(options) { * @private */ -function addListeners(stream, on, listeners) { +function addListeners (stream, on, listeners) { for (var i = 0; i < listeners.length; i++) { on.apply(stream, listeners[i]) } @@ -234,7 +242,7 @@ function addListeners(stream, on, listeners) { * Get the length of a given chunk */ -function chunkLength(chunk, encoding) { +function chunkLength (chunk, encoding) { if (!chunk) { return 0 } @@ -249,7 +257,7 @@ function chunkLength(chunk, encoding) { * @private */ -function shouldCompress(req, res) { +function shouldCompress (req, res) { var type = res.getHeader('Content-Type') if (type === undefined || !compressible(type)) { @@ -265,11 +273,11 @@ function shouldCompress(req, res) { * @private */ -function shouldTransform(req, res) { +function shouldTransform (req, res) { var cacheControl = res.getHeader('Cache-Control') // Don't compress for Cache-Control: no-transform // https://tools.ietf.org/html/rfc7234#section-5.2.2.4 - return !cacheControl - || !cacheControlNoTransformRegExp.test(cacheControl) + return !cacheControl || + !cacheControlNoTransformRegExp.test(cacheControl) } diff --git a/package.json b/package.json index 498061ba..1dfff077 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "compression", "description": "Node.js compression middleware", - "version": "1.6.1", + "version": "1.6.2", "contributors": [ "Douglas Christopher Wilson ", "Jonathan Ong (http://jongleberry.com)" @@ -9,16 +9,20 @@ "license": "MIT", "repository": "expressjs/compression", "dependencies": { - "accepts": "~1.3.1", - "bytes": "2.2.0", - "compressible": "~2.0.7", + "accepts": "~1.3.3", + "bytes": "2.3.0", + "compressible": "~2.0.8", "debug": "~2.2.0", "on-headers": "~1.0.1", "vary": "~1.1.0" }, "devDependencies": { - "istanbul": "0.4.2", - "mocha": "2.3.4", + "eslint": "2.9.0", + "eslint-config-standard": "5.3.1", + "eslint-plugin-promise": "1.1.0", + "eslint-plugin-standard": "1.3.2", + "istanbul": "0.4.3", + "mocha": "2.4.5", "supertest": "1.1.0" }, "files": [ @@ -30,6 +34,7 @@ "node": ">= 0.8.0" }, "scripts": { + "lint": "eslint **/*.js", "test": "mocha --check-leaks --reporter spec --bail", "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --check-leaks --reporter dot", "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --check-leaks --reporter spec" diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 00000000..29c15b08 --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} diff --git a/test/compression.js b/test/compression.js index 26e0ca26..06e31c66 100644 --- a/test/compression.js +++ b/test/compression.js @@ -1,13 +1,15 @@ var assert = require('assert') -var bytes = require('bytes'); -var crypto = require('crypto'); -var http = require('http'); -var request = require('supertest'); +var bytes = require('bytes') +var crypto = require('crypto') +var http = require('http') +var request = require('supertest') +var OutgoingMessage = http.OutgoingMessage +var hasCallbacks = (OutgoingMessage.prototype.write.length === 3) -var compression = require('..'); +var compression = require('..') -describe('compression()', function(){ - it('should skip HEAD', function(done){ +describe('compression()', function () { + it('should skip HEAD', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.end('hello, world') @@ -20,7 +22,7 @@ describe('compression()', function(){ .expect(200, done) }) - it('should skip unknown accept-encoding', function(done){ + it('should skip unknown accept-encoding', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.end('hello, world') @@ -33,7 +35,7 @@ describe('compression()', function(){ .expect(200, done) }) - it('should skip if content-encoding already set', function(done){ + it('should skip if content-encoding already set', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.setHeader('Content-Encoding', 'x-custom') @@ -47,7 +49,7 @@ describe('compression()', function(){ .expect(200, 'hello, world', done) }) - it('should set Vary', function(done){ + it('should set Vary', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.end('hello, world') @@ -60,7 +62,7 @@ describe('compression()', function(){ .expect('Vary', 'Accept-Encoding', done) }) - it('should set Vary even if Accept-Encoding is not set', function(done){ + it('should set Vary even if Accept-Encoding is not set', function (done) { var server = createServer({ threshold: 1000 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.end('hello, world') @@ -73,7 +75,7 @@ describe('compression()', function(){ .expect(200, done) }) - it('should not set Vary if Content-Type does not pass filter', function(done){ + it('should not set Vary if Content-Type does not pass filter', function (done) { var server = createServer(null, function (req, res) { res.setHeader('Content-Type', 'image/jpeg') res.end() @@ -85,7 +87,7 @@ describe('compression()', function(){ .expect(200, done) }) - it('should set Vary for HEAD request', function(done){ + it('should set Vary for HEAD request', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.end('hello, world') @@ -97,7 +99,7 @@ describe('compression()', function(){ .expect('Vary', 'Accept-Encoding', done) }) - it('should transfer chunked', function(done){ + it('should transfer chunked', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.end('hello, world') @@ -109,7 +111,7 @@ describe('compression()', function(){ .expect('Transfer-Encoding', 'chunked', done) }) - it('should remove Content-Length for chunked', function(done){ + it('should remove Content-Length for chunked', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.end('hello, world') @@ -136,7 +138,7 @@ describe('compression()', function(){ .expect(200, 'hello, world', done) }) - it('should allow writing after close', function(done){ + it('should allow writing after close', function (done) { // UGH var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') @@ -150,17 +152,17 @@ describe('compression()', function(){ request(server) .get('/') - .end(function(){}) + .end(function () {}) }) - it('should back-pressure when compressed', function(done){ + it('should back-pressure when compressed', function (done) { var buf var client var drained = false var resp var server = createServer({ threshold: 0 }, function (req, res) { resp = res - res.on('drain', function(){ + res.on('drain', function () { drained = true }) res.setHeader('Content-Type', 'text/plain') @@ -169,25 +171,26 @@ describe('compression()', function(){ }) var wait = 2 - crypto.pseudoRandomBytes(1024 * 128, function(err, chunk){ + crypto.pseudoRandomBytes(1024 * 128, function (err, chunk) { + if (err) return done(err) buf = chunk pressure() }) - function complete(){ + function complete () { if (--wait !== 0) return assert.ok(drained) done() } - function pressure(){ + function pressure () { if (!buf || !resp || !client) return while (resp.write(buf) !== false) { resp.flush() } - resp.on('drain', function(){ + resp.on('drain', function () { resp.write('end') resp.end() }) @@ -208,14 +211,14 @@ describe('compression()', function(){ .end() }) - it('should back-pressure when uncompressed', function(done){ + it('should back-pressure when uncompressed', function (done) { var buf var client var drained = false var resp - var server = createServer({ filter: function(){ return false } }, function (req, res) { + var server = createServer({ filter: function () { return false } }, function (req, res) { resp = res - res.on('drain', function(){ + res.on('drain', function () { drained = true }) res.setHeader('Content-Type', 'text/plain') @@ -224,25 +227,26 @@ describe('compression()', function(){ }) var wait = 2 - crypto.pseudoRandomBytes(1024 * 128, function(err, chunk){ + crypto.pseudoRandomBytes(1024 * 128, function (err, chunk) { + if (err) return done(err) buf = chunk pressure() }) - function complete(){ + function complete () { if (--wait !== 0) return assert.ok(drained) done() } - function pressure(){ + function pressure () { if (!buf || !resp || !client) return while (resp.write(buf) !== false) { resp.flush() } - resp.on('drain', function(){ + resp.on('drain', function () { resp.write('end') resp.end() }) @@ -304,8 +308,8 @@ describe('compression()', function(){ .expect(200, done) }) - describe('threshold', function(){ - it('should not compress responses below the threshold size', function(done){ + describe('threshold', function () { + it('should not compress responses below the threshold size', function (done) { var server = createServer({ threshold: '1kb' }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.setHeader('Content-Length', '12') @@ -319,7 +323,7 @@ describe('compression()', function(){ .expect(200, done) }) - it('should compress responses above the threshold size', function(done){ + it('should compress responses above the threshold size', function (done) { var server = createServer({ threshold: '1kb' }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.setHeader('Content-Length', '2048') @@ -332,11 +336,11 @@ describe('compression()', function(){ .expect('Content-Encoding', 'gzip', done) }) - it('should compress when streaming without a content-length', function(done){ + it('should compress when streaming without a content-length', function (done) { var server = createServer({ threshold: '1kb' }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.write('hello, ') - setTimeout(function(){ + setTimeout(function () { res.end('world') }, 10) }) @@ -347,12 +351,12 @@ describe('compression()', function(){ .expect('Content-Encoding', 'gzip', done) }) - it('should not compress when streaming and content-length is lower than threshold', function(done){ + it('should not compress when streaming and content-length is lower than threshold', function (done) { var server = createServer({ threshold: '1kb' }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.setHeader('Content-Length', '12') res.write('hello, ') - setTimeout(function(){ + setTimeout(function () { res.end('world') }, 10) }) @@ -364,12 +368,12 @@ describe('compression()', function(){ .expect(200, done) }) - it('should compress when streaming and content-length is larger than threshold', function(done){ + it('should compress when streaming and content-length is larger than threshold', function (done) { var server = createServer({ threshold: '1kb' }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.setHeader('Content-Length', '2048') res.write(new Buffer(1024)) - setTimeout(function(){ + setTimeout(function () { res.end(new Buffer(1024)) }, 10) }) @@ -382,7 +386,7 @@ describe('compression()', function(){ // res.end(str, encoding) broken in node.js 0.8 var run = /^v0\.8\./.test(process.version) ? it.skip : it - run('should handle writing hex data', function(done){ + run('should handle writing hex data', function (done) { var server = createServer({ threshold: 6 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.end('2e2e2e2e', 'hex') @@ -395,7 +399,7 @@ describe('compression()', function(){ .expect(200, '....', done) }) - it('should consider res.end() as 0 length', function(done){ + it('should consider res.end() as 0 length', function (done) { var server = createServer({ threshold: 1 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') res.end() @@ -587,7 +591,7 @@ describe('compression()', function(){ write() }) - function write() { + function write () { chunks++ if (chunks === 2) return resp.end() if (chunks > 2) return chunks-- @@ -602,7 +606,7 @@ describe('compression()', function(){ .on('response', function (res) { assert.equal(res.headers['content-encoding'], 'gzip') res.on('data', write) - res.on('end', function(){ + res.on('end', function () { assert.equal(chunks, 2) done() }) @@ -619,7 +623,7 @@ describe('compression()', function(){ write() }) - function write() { + function write () { chunks++ if (chunks === 20) return resp.end() if (chunks > 20) return chunks-- @@ -634,7 +638,7 @@ describe('compression()', function(){ .on('response', function (res) { assert.equal(res.headers['content-encoding'], 'gzip') res.on('data', write) - res.on('end', function(){ + res.on('end', function () { assert.equal(chunks, 20) done() }) @@ -651,7 +655,7 @@ describe('compression()', function(){ write() }) - function write() { + function write () { chunks++ if (chunks === 20) return resp.end() if (chunks > 20) return chunks-- @@ -666,7 +670,7 @@ describe('compression()', function(){ .on('response', function (res) { assert.equal(res.headers['content-encoding'], 'deflate') res.on('data', write) - res.on('end', function(){ + res.on('end', function () { assert.equal(chunks, 20) done() }) @@ -674,16 +678,82 @@ describe('compression()', function(){ .end() }) }) + + describe('when callbacks are used', function () { + it('should call the passed callbacks in the order passed when compressing', function (done) { + var callbackOutput = [] + var server = createServer(null, function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.write('Hello', null, function () { + callbackOutput.push(0) + }) + res.write(' World', null, function () { + callbackOutput.push(1) + }) + res.end(null, null, function () { + callbackOutput.push(2) + }) + }) + + request(server) + .get('/') + .set('Accept-Encoding', 'gzip') + .expect('Content-Encoding', 'gzip') + .end(function (err) { + if (err) { + throw new Error(err) + } + if (hasCallbacks) { + assert.equal(callbackOutput.length, 3) + assert.deepEqual(callbackOutput, [0, 1, 2]) + } + done() + }) + }) + + it('should call the passed callbacks in the order passed when not compressing', function (done) { + var callbackOutput = [] + var server = createServer(null, function (req, res) { + res.setHeader('Cache-Control', 'no-transform') + res.setHeader('Content-Type', 'text/plain') + res.write('hello,', null, function () { + callbackOutput.push(0) + }) + res.write(' world', null, function () { + callbackOutput.push(1) + }) + res.end(null, null, function () { + callbackOutput.push(2) + }) + }) + + request(server) + .get('/') + .set('Accept-Encoding', 'gzip') + .expect('Cache-Control', 'no-transform') + .expect(shouldNotHaveHeader('Content-Encoding')) + .end(function (err) { + if (err) { + throw new Error(err) + } + if (hasCallbacks) { + assert.equal(callbackOutput.length, 3) + assert.deepEqual(callbackOutput, [0, 1, 2]) + } + done() + }) + }) + }) }) -function createServer(opts, fn) { +function createServer (opts, fn) { var _compression = compression(opts) return http.createServer(function (req, res) { _compression(req, res, function (err) { if (err) { res.statusCode = err.status || 500 res.end(err.message) - return; + return } fn(req, res) @@ -691,13 +761,13 @@ function createServer(opts, fn) { }) } -function shouldHaveBodyLength(length) { +function shouldHaveBodyLength (length) { return function (res) { assert.equal(res.text.length, length, 'should have body length of ' + length) } } -function shouldNotHaveHeader(header) { +function shouldNotHaveHeader (header) { return function (res) { assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header) }