From 5bdbb9e8d89d7c219071f1327cd153bf91de7d90 Mon Sep 17 00:00:00 2001 From: Daniel Cohen Gindi Date: Fri, 10 Jul 2020 12:47:04 +0300 Subject: [PATCH 01/18] Added support for brotli ('br') content-encoding --- README.md | 6 +++++ index.js | 35 +++++++++++++++++++++--- package.json | 1 + test/compression.js | 66 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 680ece87..d42e09fe 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ The following compression codings are supported: - deflate - gzip + - br (brotli) + +**Note** Brotli provides better and faster compression then gzip or deflate, but is supported only since Node.js versions v11.7.0 and v10.16.0. ## Install @@ -46,6 +49,9 @@ as compressing will transform the body. those listed below, [zlib](http://nodejs.org/api/zlib.html) options may be passed in to the options object. +As for *brotli*, a default is set to compression level 4, unless +[anything else is specified](https://nodejs.org/api/zlib.html#zlib_class_brotlioptions). + ##### chunkSize The default value is `zlib.Z_DEFAULT_CHUNK`, or `16384`. diff --git a/index.js b/index.js index 1d089427..4b098591 100644 --- a/index.js +++ b/index.js @@ -19,6 +19,7 @@ var Buffer = require('safe-buffer').Buffer var bytes = require('bytes') var compressible = require('compressible') var debug = require('debug')('compression') +var objectAssign = require('object-assign') var onHeaders = require('on-headers') var vary = require('vary') var zlib = require('zlib') @@ -37,6 +38,24 @@ module.exports.filter = shouldCompress var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/ +/** + * @const + * whether current node version has brotli support + */ +var hasBrotliSupport = 'brotli' in process.versions + +var supportedEncodings = hasBrotliSupport + ? ['gzip', 'deflate', 'br', 'identity'] + : ['gzip', 'deflate', 'identity'] + +var supportedCompressionsNoDeflate = hasBrotliSupport + ? ['gzip', 'br'] + : ['gzip'] + +var supportedEncodingsNoDeflate = hasBrotliSupport + ? ['gzip', 'br', 'identity'] + : ['gzip', 'identity'] + /** * Compress response data with gzip / deflate. * @@ -48,6 +67,12 @@ var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/ function compression (options) { var opts = options || {} + if (hasBrotliSupport && opts.params === undefined) { + opts = objectAssign({}, opts) + opts.params = {} + opts.params[zlib.constants.BROTLI_PARAM_QUALITY] = 4 + } + // options var filter = opts.filter || shouldCompress var threshold = bytes.parse(opts.threshold) @@ -175,11 +200,11 @@ function compression (options) { // compression method var accept = accepts(req) - var method = accept.encoding(['gzip', 'deflate', 'identity']) + var method = accept.encoding(supportedEncodings) // we really don't prefer deflate - if (method === 'deflate' && accept.encoding(['gzip'])) { - method = accept.encoding(['gzip', 'identity']) + if (method === 'deflate' && accept.encoding(supportedCompressionsNoDeflate)) { + method = accept.encoding(supportedEncodingsNoDeflate) } // negotiation failed @@ -192,7 +217,9 @@ function compression (options) { debug('%s compression', method) stream = method === 'gzip' ? zlib.createGzip(opts) - : zlib.createDeflate(opts) + : method === 'br' + ? zlib.createBrotliCompress(opts) + : zlib.createDeflate(opts) // add buffered listeners to stream addListeners(stream, stream.on, listeners) diff --git a/package.json b/package.json index 30f8422c..58196d81 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "bytes": "3.0.0", "compressible": "~2.0.17", "debug": "2.6.9", + "object-assign": "4.1.1", "on-headers": "~1.0.2", "safe-buffer": "5.2.0", "vary": "~1.1.2" diff --git a/test/compression.js b/test/compression.js index 6975ea0b..1bcb5680 100644 --- a/test/compression.js +++ b/test/compression.js @@ -9,6 +9,12 @@ var zlib = require('zlib') var compression = require('..') +/** + * @const + * whether current node version has brotli support + */ +var hasBrotliSupport = 'brotli' in process.versions + describe('compression()', function () { it('should skip HEAD', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { @@ -465,6 +471,21 @@ describe('compression()', function () { }) }) + describe('when "Accept-Encoding: br"', function () { + var brotlit = hasBrotliSupport ? it : it.skip + brotlit('should respond with br', function (done) { + var server = createServer({ threshold: 0 }, function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.end('hello, world') + }) + + request(server) + .get('/') + .set('Accept-Encoding', 'br') + .expect('Content-Encoding', 'br', done) + }) + }) + describe('when "Accept-Encoding: gzip, deflate"', function () { it('should respond with gzip', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { @@ -493,6 +514,21 @@ describe('compression()', function () { }) }) + describe('when "Accept-Encoding: deflate, br"', function () { + var brotlit = hasBrotliSupport ? it : it.skip + brotlit('should respond with br', function (done) { + var server = createServer({ threshold: 0 }, function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.end('hello, world') + }) + + request(server) + .get('/') + .set('Accept-Encoding', 'deflate, br') + .expect('Content-Encoding', 'br', done) + }) + }) + describe('when "Cache-Control: no-transform" response header', function () { it('should not compress response', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { @@ -631,6 +667,33 @@ describe('compression()', function () { .end() }) + var brotlit = hasBrotliSupport ? it : it.skip + brotlit('should flush small chunks for brotli', function (done) { + var chunks = 0 + var next + var server = createServer({ threshold: 0 }, function (req, res) { + next = writeAndFlush(res, 2, Buffer.from('..')) + res.setHeader('Content-Type', 'text/plain') + next() + }) + + function onchunk (chunk) { + assert.ok(chunks++ < 20) + assert.strictEqual(chunk.toString(), '..') + next() + } + + request(server) + .get('/') + .set('Accept-Encoding', 'br') + .request() + .on('response', unchunk('br', onchunk, function (err) { + if (err) return done(err) + server.close(done) + })) + .end() + }) + it('should flush small chunks for deflate', function (done) { var chunks = 0 var next @@ -710,6 +773,9 @@ function unchunk (encoding, onchunk, onend) { case 'gzip': stream = res.pipe(zlib.createGunzip()) break + case 'br': + stream = res.pipe(zlib.createBrotliDecompress()) + break } stream.on('data', onchunk) From 6ef8cef9be7e9199abe5a5111ac5983e1b1f4402 Mon Sep 17 00:00:00 2001 From: Daniel Cohen Gindi Date: Sun, 12 Jul 2020 22:46:47 +0300 Subject: [PATCH 02/18] Update README.md --- README.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d42e09fe..73a3437a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The following compression codings are supported: - gzip - br (brotli) -**Note** Brotli provides better and faster compression then gzip or deflate, but is supported only since Node.js versions v11.7.0 and v10.16.0. +**Note** Brotli provides [better and faster compression then gzip or deflate](https://expeditedsecurity.com/blog/nginx-brotli/), but is supported only since Node.js versions v11.7.0 and v10.16.0. ## Install @@ -47,10 +47,12 @@ as compressing will transform the body. `compression()` accepts these properties in the options object. In addition to those listed below, [zlib](http://nodejs.org/api/zlib.html) options may be -passed in to the options object. +passed in to the options object or +[brotli](https://nodejs.org/api/zlib.html#zlib_class_brotlioptions) options. -As for *brotli*, a default is set to compression level 4, unless -[anything else is specified](https://nodejs.org/api/zlib.html#zlib_class_brotlioptions). +As for *brotli*, a default is set to compression level 4, unless anything else +is specified. This is a balanced setting with [a very good speed and a very good +compression ratio](https://expeditedsecurity.com/blog/nginx-brotli/). ##### chunkSize @@ -147,6 +149,17 @@ The default value is `zlib.Z_DEFAULT_WINDOWBITS`, or `15`. See [Node.js documentation](http://nodejs.org/api/zlib.html#zlib_memory_usage_tuning) regarding the usage. +##### params [*(brotli only)*](https://nodejs.org/api/zlib.html#zlib_brotli_constants) + + - `zlib.constants.BROTLI_PARAM_MODE` + - `zlib.constants.BROTLI_MODE_GENERIC` (default) + - `zlib.constants.BROTLI_MODE_TEXT`, adjusted for UTF-8 text + - `zlib.constants.BROTLI_MODE_FONT`, adjusted for WOFF 2.0 fonts + - `zlib.constants.BROTLI_PARAM_QUALITY` + - Ranges from `zlib.constants.BROTLI_MIN_QUALITY` to + `zlib.constants.BROTLI_MAX_QUALITY`, with a default of + `4` (which is not node's default but the most optimal). + #### .filter The default `filter` function. This is used to construct a custom filter From 02c06c275d246ea63811d44698241de5d338392c Mon Sep 17 00:00:00 2001 From: Daniel Cohen Gindi Date: Mon, 13 Jul 2020 22:22:35 +0300 Subject: [PATCH 03/18] Update README.md --- README.md | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 73a3437a..d871d203 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The following compression codings are supported: - gzip - br (brotli) -**Note** Brotli provides [better and faster compression then gzip or deflate](https://expeditedsecurity.com/blog/nginx-brotli/), but is supported only since Node.js versions v11.7.0 and v10.16.0. +**Note** Brotli is supported only since Node.js versions v11.7.0 and v10.16.0. ## Install @@ -50,10 +50,6 @@ those listed below, [zlib](http://nodejs.org/api/zlib.html) options may be passed in to the options object or [brotli](https://nodejs.org/api/zlib.html#zlib_class_brotlioptions) options. -As for *brotli*, a default is set to compression level 4, unless anything else -is specified. This is a balanced setting with [a very good speed and a very good -compression ratio](https://expeditedsecurity.com/blog/nginx-brotli/). - ##### chunkSize The default value is `zlib.Z_DEFAULT_CHUNK`, or `16384`. @@ -109,6 +105,20 @@ The default value is `zlib.Z_DEFAULT_MEMLEVEL`, or `8`. See [Node.js documentation](http://nodejs.org/api/zlib.html#zlib_memory_usage_tuning) regarding the usage. +##### params [*(brotli only)*](https://nodejs.org/api/zlib.html#zlib_brotli_constants) + + - `zlib.constants.BROTLI_PARAM_MODE` + - `zlib.constants.BROTLI_MODE_GENERIC` (default) + - `zlib.constants.BROTLI_MODE_TEXT`, adjusted for UTF-8 text + - `zlib.constants.BROTLI_MODE_FONT`, adjusted for WOFF 2.0 fonts + - `zlib.constants.BROTLI_PARAM_QUALITY` + - Ranges from `zlib.constants.BROTLI_MIN_QUALITY` to + `zlib.constants.BROTLI_MAX_QUALITY`, with a default of + `4` (which is not node's default but the most optimal). + +Note that here the default is set to compression level 4. This is a balanced setting with a very good speed and a very good +compression ratio. + ##### strategy This is used to tune the compression algorithm. This value only affects the @@ -149,17 +159,6 @@ The default value is `zlib.Z_DEFAULT_WINDOWBITS`, or `15`. See [Node.js documentation](http://nodejs.org/api/zlib.html#zlib_memory_usage_tuning) regarding the usage. -##### params [*(brotli only)*](https://nodejs.org/api/zlib.html#zlib_brotli_constants) - - - `zlib.constants.BROTLI_PARAM_MODE` - - `zlib.constants.BROTLI_MODE_GENERIC` (default) - - `zlib.constants.BROTLI_MODE_TEXT`, adjusted for UTF-8 text - - `zlib.constants.BROTLI_MODE_FONT`, adjusted for WOFF 2.0 fonts - - `zlib.constants.BROTLI_PARAM_QUALITY` - - Ranges from `zlib.constants.BROTLI_MIN_QUALITY` to - `zlib.constants.BROTLI_MAX_QUALITY`, with a default of - `4` (which is not node's default but the most optimal). - #### .filter The default `filter` function. This is used to construct a custom filter From 4df713b36b7e0bc742bc745f0cb4c41299cf3d3f Mon Sep 17 00:00:00 2001 From: Daniel Cohen Gindi Date: Mon, 13 Jul 2020 22:25:58 +0300 Subject: [PATCH 04/18] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d871d203..e2ce8c65 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ The default value is `zlib.Z_DEFAULT_MEMLEVEL`, or `8`. See [Node.js documentation](http://nodejs.org/api/zlib.html#zlib_memory_usage_tuning) regarding the usage. -##### params [*(brotli only)*](https://nodejs.org/api/zlib.html#zlib_brotli_constants) +##### params *(brotli only)* - [key-value object containing indexed Brotli parameters](https://nodejs.org/api/zlib.html#zlib_brotli_constants) - `zlib.constants.BROTLI_PARAM_MODE` - `zlib.constants.BROTLI_MODE_GENERIC` (default) From 87076afbab0f6f2c5358ff99dc9d3d28e7a1cd25 Mon Sep 17 00:00:00 2001 From: Daniel Cohen Gindi Date: Mon, 13 Jul 2020 22:30:02 +0300 Subject: [PATCH 05/18] Apply default value also when params is specified --- index.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 4b098591..8e42b110 100644 --- a/index.js +++ b/index.js @@ -67,10 +67,17 @@ var supportedEncodingsNoDeflate = hasBrotliSupport function compression (options) { var opts = options || {} - if (hasBrotliSupport && opts.params === undefined) { - opts = objectAssign({}, opts) - opts.params = {} - opts.params[zlib.constants.BROTLI_PARAM_QUALITY] = 4 + if (hasBrotliSupport) { + // set the default level to a reasonable value with balanced speed/ratio + if (opts.params === undefined) { + opts = objectAssign({}, opts) + opts.params = {} + } + + if (opts.params[zlib.constants.BROTLI_PARAM_QUALITY] === undefined) { + opts.params = objectAssign({}, opts.params) + opts.params[zlib.constants.BROTLI_PARAM_QUALITY] = 4 + } } // options From fffe4c794b773e7a52735ce39de3a77b04d09eac Mon Sep 17 00:00:00 2001 From: Daniel Cohen Gindi Date: Tue, 14 Jul 2020 10:43:13 +0300 Subject: [PATCH 06/18] Increase coverage for specifying params --- test/compression.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/compression.js b/test/compression.js index 1bcb5680..1771718b 100644 --- a/test/compression.js +++ b/test/compression.js @@ -486,6 +486,24 @@ describe('compression()', function () { }) }) + describe('when "Accept-Encoding: br" and passing compression level', function () { + var brotlit = hasBrotliSupport ? it : it.skip + brotlit('should respond with br', function (done) { + var params = {} + params[zlib.constants.BROTLI_PARAM_QUALITY] = 11 + + var server = createServer({ threshold: 0, params: params }, function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.end('hello, world') + }) + + request(server) + .get('/') + .set('Accept-Encoding', 'br') + .expect('Content-Encoding', 'br', done) + }) + }) + describe('when "Accept-Encoding: gzip, deflate"', function () { it('should respond with gzip', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { From 9145a551eb3ae9a2fe138ca2abb0b6741c4680c2 Mon Sep 17 00:00:00 2001 From: Daniel Cohen Gindi Date: Tue, 25 Aug 2020 12:29:08 +0300 Subject: [PATCH 07/18] Updated brotli detection method --- index.js | 2 +- test/compression.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 8e42b110..36c1a632 100644 --- a/index.js +++ b/index.js @@ -42,7 +42,7 @@ var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/ * @const * whether current node version has brotli support */ -var hasBrotliSupport = 'brotli' in process.versions +var hasBrotliSupport = 'createBrotliCompress' in zlib var supportedEncodings = hasBrotliSupport ? ['gzip', 'deflate', 'br', 'identity'] diff --git a/test/compression.js b/test/compression.js index 1771718b..4c019062 100644 --- a/test/compression.js +++ b/test/compression.js @@ -13,7 +13,7 @@ var compression = require('..') * @const * whether current node version has brotli support */ -var hasBrotliSupport = 'brotli' in process.versions +var hasBrotliSupport = 'createBrotliCompress' in zlib describe('compression()', function () { it('should skip HEAD', function (done) { From 0bb402ee417a661ddc0797f4141ff214b7c480ae Mon Sep 17 00:00:00 2001 From: Daniel Cohen Gindi Date: Sun, 30 Aug 2020 17:17:35 +0300 Subject: [PATCH 08/18] Prefer br over gzip and deflate --- index.js | 21 +++++++++++---------- test/compression.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 36c1a632..8f3d52b5 100644 --- a/index.js +++ b/index.js @@ -45,17 +45,13 @@ var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/ var hasBrotliSupport = 'createBrotliCompress' in zlib var supportedEncodings = hasBrotliSupport - ? ['gzip', 'deflate', 'br', 'identity'] + ? ['br', 'gzip', 'deflate', 'identity'] : ['gzip', 'deflate', 'identity'] -var supportedCompressionsNoDeflate = hasBrotliSupport - ? ['gzip', 'br'] +var preferredEncodings = hasBrotliSupport + ? ['br', 'gzip'] : ['gzip'] -var supportedEncodingsNoDeflate = hasBrotliSupport - ? ['gzip', 'br', 'identity'] - : ['gzip', 'identity'] - /** * Compress response data with gzip / deflate. * @@ -209,9 +205,14 @@ function compression (options) { var accept = accepts(req) var method = accept.encoding(supportedEncodings) - // we really don't prefer deflate - if (method === 'deflate' && accept.encoding(supportedCompressionsNoDeflate)) { - method = accept.encoding(supportedEncodingsNoDeflate) + // we have our own set of preferences, override user-agent preferences + for (var i = 0, len = preferredEncodings.length, preferred; i < len; i++) { + preferred = preferredEncodings[i] + + if (method !== preferred && accept.encoding(preferred)) { + method = preferred + break + } } // negotiation failed diff --git a/test/compression.js b/test/compression.js index 4c019062..cf24346e 100644 --- a/test/compression.js +++ b/test/compression.js @@ -532,6 +532,36 @@ describe('compression()', function () { }) }) + describe('when "Accept-Encoding: gzip, br"', function () { + var brotlit = hasBrotliSupport ? it : it.skip + brotlit('should respond with br', function (done) { + var server = createServer({ threshold: 0 }, function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.end('hello, world') + }) + + request(server) + .get('/') + .set('Accept-Encoding', 'gzip, br') + .expect('Content-Encoding', 'br', done) + }) + }) + + describe('when "Accept-Encoding: deflate, gzip, br"', function () { + var brotlit = hasBrotliSupport ? it : it.skip + brotlit('should respond with br', function (done) { + var server = createServer({ threshold: 0 }, function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.end('hello, world') + }) + + request(server) + .get('/') + .set('Accept-Encoding', 'deflate, gzip, br') + .expect('Content-Encoding', 'br', done) + }) + }) + describe('when "Accept-Encoding: deflate, br"', function () { var brotlit = hasBrotliSupport ? it : it.skip brotlit('should respond with br', function (done) { From bbcd9c465a5524a83fc54700a0c6cb6de8d4dddf Mon Sep 17 00:00:00 2001 From: Nick Randall Date: Mon, 14 Sep 2020 17:18:20 -0400 Subject: [PATCH 09/18] feat: use "koa-compress" logic to determine the preferred encoding --- index.js | 20 +++++++------------- package.json | 1 + test/compression.js | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index 8f3d52b5..f7ef1984 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,8 @@ * @private */ -var accepts = require('accepts') +// var accepts = require('accepts') +var Encodings = require('koa-compress/lib/encodings'); var Buffer = require('safe-buffer').Buffer var bytes = require('bytes') var compressible = require('compressible') @@ -202,18 +203,11 @@ function compression (options) { } // compression method - var accept = accepts(req) - var method = accept.encoding(supportedEncodings) - - // we have our own set of preferences, override user-agent preferences - for (var i = 0, len = preferredEncodings.length, preferred; i < len; i++) { - preferred = preferredEncodings[i] - - if (method !== preferred && accept.encoding(preferred)) { - method = preferred - break - } - } + var encodings = new Encodings({ + preferredEncodings: supportedEncodings + }) + encodings.parseAcceptEncoding(req.headers['accept-encoding'] || 'identity') + var method = encodings.getPreferredContentEncoding() // negotiation failed if (!method || method === 'identity') { diff --git a/package.json b/package.json index 58196d81..1fa87f94 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "bytes": "3.0.0", "compressible": "~2.0.17", "debug": "2.6.9", + "koa-compress": "^4.0.1", "object-assign": "4.1.1", "on-headers": "~1.0.2", "safe-buffer": "5.2.0", diff --git a/test/compression.js b/test/compression.js index cf24346e..0aedaec8 100644 --- a/test/compression.js +++ b/test/compression.js @@ -562,6 +562,21 @@ describe('compression()', function () { }) }) + describe('when "Accept-Encoding: gzip;q=1, br;q=0.3"', function () { + var brotlit = hasBrotliSupport ? it : it.skip + brotlit('should respond with gzip', function (done) { + var server = createServer({ threshold: 0 }, function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.end('hello, world') + }) + + request(server) + .get('/') + .set('Accept-Encoding', 'gzip;q=1, br;q=0.3') + .expect('Content-Encoding', 'gzip', done) + }) + }) + describe('when "Accept-Encoding: deflate, br"', function () { var brotlit = hasBrotliSupport ? it : it.skip brotlit('should respond with br', function (done) { From 767c62aa94f8adaf6f1f0fcdb60cc76c050c069f Mon Sep 17 00:00:00 2001 From: Nick Randall Date: Mon, 14 Sep 2020 17:26:50 -0400 Subject: [PATCH 10/18] test: adding one more test case br/gzip with quality params --- test/compression.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/compression.js b/test/compression.js index 0aedaec8..329f13df 100644 --- a/test/compression.js +++ b/test/compression.js @@ -577,6 +577,21 @@ describe('compression()', function () { }) }) + describe('when "Accept-Encoding: gzip, br;q=0.8"', function () { + var brotlit = hasBrotliSupport ? it : it.skip + brotlit('should respond with gzip', function (done) { + var server = createServer({ threshold: 0 }, function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.end('hello, world') + }) + + request(server) + .get('/') + .set('Accept-Encoding', 'gzip, br;q=0.8') + .expect('Content-Encoding', 'gzip', done) + }) + }) + describe('when "Accept-Encoding: deflate, br"', function () { var brotlit = hasBrotliSupport ? it : it.skip brotlit('should respond with br', function (done) { From 78ad84a1e5a2adcf370338be30267ef85d9f4e65 Mon Sep 17 00:00:00 2001 From: Nick Randall Date: Mon, 14 Sep 2020 17:33:46 -0400 Subject: [PATCH 11/18] chore: fix linting errors --- index.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/index.js b/index.js index f7ef1984..8d76452b 100644 --- a/index.js +++ b/index.js @@ -14,8 +14,7 @@ * @private */ -// var accepts = require('accepts') -var Encodings = require('koa-compress/lib/encodings'); +var Encodings = require('koa-compress/lib/encodings') var Buffer = require('safe-buffer').Buffer var bytes = require('bytes') var compressible = require('compressible') @@ -49,10 +48,6 @@ var supportedEncodings = hasBrotliSupport ? ['br', 'gzip', 'deflate', 'identity'] : ['gzip', 'deflate', 'identity'] -var preferredEncodings = hasBrotliSupport - ? ['br', 'gzip'] - : ['gzip'] - /** * Compress response data with gzip / deflate. * From 4c359b86e5121a6d699fd5e651a492b4006c7a1a Mon Sep 17 00:00:00 2001 From: Nick Randall Date: Mon, 14 Sep 2020 18:55:18 -0400 Subject: [PATCH 12/18] fix: hand write encodings lib to be compatible with node 0.8 --- index.js | 19 ++------- lib/encodings.js | 103 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 - 3 files changed, 107 insertions(+), 16 deletions(-) create mode 100644 lib/encodings.js diff --git a/index.js b/index.js index 8d76452b..27bd823c 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,6 @@ * @private */ -var Encodings = require('koa-compress/lib/encodings') var Buffer = require('safe-buffer').Buffer var bytes = require('bytes') var compressible = require('compressible') @@ -24,6 +23,8 @@ var onHeaders = require('on-headers') var vary = require('vary') var zlib = require('zlib') +var Encodings = require('./lib/encodings') + /** * Module exports. */ @@ -38,16 +39,6 @@ module.exports.filter = shouldCompress var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/ -/** - * @const - * whether current node version has brotli support - */ -var hasBrotliSupport = 'createBrotliCompress' in zlib - -var supportedEncodings = hasBrotliSupport - ? ['br', 'gzip', 'deflate', 'identity'] - : ['gzip', 'deflate', 'identity'] - /** * Compress response data with gzip / deflate. * @@ -59,7 +50,7 @@ var supportedEncodings = hasBrotliSupport function compression (options) { var opts = options || {} - if (hasBrotliSupport) { + if (Encodings.hasBrotliSupport) { // set the default level to a reasonable value with balanced speed/ratio if (opts.params === undefined) { opts = objectAssign({}, opts) @@ -198,9 +189,7 @@ function compression (options) { } // compression method - var encodings = new Encodings({ - preferredEncodings: supportedEncodings - }) + var encodings = new Encodings(); encodings.parseAcceptEncoding(req.headers['accept-encoding'] || 'identity') var method = encodings.getPreferredContentEncoding() diff --git a/lib/encodings.js b/lib/encodings.js new file mode 100644 index 00000000..7643db29 --- /dev/null +++ b/lib/encodings.js @@ -0,0 +1,103 @@ +// NOTE: Most of this code was ported from "koa-compress" +// See: https://github.com/koajs/compress + +"use strict"; +var zlib = require("zlib"); + +module.exports = Encodings; + +/** + * @const + * whether current node version has brotli support + */ +var hasBrotliSupport = "createBrotliCompress" in zlib; + +function Encodings() { + this.encodingWeights = []; +} + +Encodings.supportedEncodings = { + gzip: true, + deflate: true, + identity: true +}; + +Encodings.preferredEncodings = ["br", "gzip", "deflate", "identity"]; + +if (hasBrotliSupport) { + Encodings.supportedEncodings.br = true; +} + +Encodings.reDirective = /^\s*(gzip|compress|deflate|br|identity|\*)\s*(?:;\s*q\s*=\s*(\d(?:\.\d)?))?\s*$/; + +Encodings.hasBrotliSupport = hasBrotliSupport; + +Encodings.prototype.parseAcceptEncoding = function (acceptEncoding) { + var acceptEncoding = acceptEncoding || ""; + + var encodingWeights = this.encodingWeights, + reDirective = Encodings.reDirective; + acceptEncoding.split(",").forEach(function (directive) { + var match = reDirective.exec(directive); + if (!match) return; // not a supported encoding above + + var encoding = match[1]; + + // weight must be in [0, 1] + var weight = match[2] && !isNaN(match[2]) ? parseFloat(match[2], 10) : 1; + weight = Math.max(weight, 0); + weight = Math.min(weight, 1); + + encodingWeights.push({ encoding: encoding, weight: weight }); + }); +}; + +Encodings.prototype.getPreferredContentEncoding = function () { + var encodingWeights = this.encodingWeights; + + var acceptedEncodings = encodingWeights + // sort by weight + .sort(function (a, b) { + return b.weight - a.weight; + }) + // filter by supported encodings + .filter(function (record) { + return Encodings.supportedEncodings[record.encoding]; + }); + + // group them by weights + var weightClasses = {}; + var weightList = []; + acceptedEncodings.forEach(function (record) { + var weight = record.weight; + if (!weightClasses.hasOwnProperty(weight)) { + weightClasses[weight] = []; + weightList.push(weight); + } + weightClasses[weight].push(record.encoding); + }); + + // search by weight, descending + var weights = weightList.sort(function (a, b) { + return b - a; + }); + + for (var i = 0; i < weights.length; i++) { + // encodings at this weight + var encodings = weightClasses[weights[i]]; + + // return the first encoding in the preferred list + for (var j = 0; j < Encodings.preferredEncodings.length; j++) { + var preferredEncoding = Encodings.preferredEncodings[j]; + if (encodings.indexOf(preferredEncoding) >= 0) return preferredEncoding; + } + } + + // no encoding matches, check to see if the client set identity, q=0 + if (encodingWeights["identity"] && encodingWeights["identity"].weight === 0) { + throw new Error("Please accept br, gzip, deflate, or identity."); + } + + // by default, return nothing + return "identity"; +}; diff --git a/package.json b/package.json index 1fa87f94..58196d81 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "bytes": "3.0.0", "compressible": "~2.0.17", "debug": "2.6.9", - "koa-compress": "^4.0.1", "object-assign": "4.1.1", "on-headers": "~1.0.2", "safe-buffer": "5.2.0", From 8340cdefbac062ee558bd0b3c7565c9d48eca398 Mon Sep 17 00:00:00 2001 From: Nick Randall Date: Mon, 14 Sep 2020 19:28:37 -0400 Subject: [PATCH 13/18] Fix: fixing lint errors in new lib --- index.js | 2 +- lib/encodings.js | 89 +++++++++++++++++++++------------------------ test/compression.js | 15 ++++++++ 3 files changed, 58 insertions(+), 48 deletions(-) diff --git a/index.js b/index.js index 27bd823c..adfc01cd 100644 --- a/index.js +++ b/index.js @@ -189,7 +189,7 @@ function compression (options) { } // compression method - var encodings = new Encodings(); + var encodings = new Encodings() encodings.parseAcceptEncoding(req.headers['accept-encoding'] || 'identity') var method = encodings.getPreferredContentEncoding() diff --git a/lib/encodings.js b/lib/encodings.js index 7643db29..7d43fa5d 100644 --- a/lib/encodings.js +++ b/lib/encodings.js @@ -1,103 +1,98 @@ // NOTE: Most of this code was ported from "koa-compress" // See: https://github.com/koajs/compress -"use strict"; -var zlib = require("zlib"); +'use strict' +var zlib = require('zlib') -module.exports = Encodings; +module.exports = Encodings /** * @const * whether current node version has brotli support */ -var hasBrotliSupport = "createBrotliCompress" in zlib; +var hasBrotliSupport = 'createBrotliCompress' in zlib -function Encodings() { - this.encodingWeights = []; +function Encodings () { + this.encodingWeights = [] } Encodings.supportedEncodings = { gzip: true, deflate: true, identity: true -}; +} -Encodings.preferredEncodings = ["br", "gzip", "deflate", "identity"]; +Encodings.preferredEncodings = ['br', 'gzip', 'deflate', 'identity'] if (hasBrotliSupport) { - Encodings.supportedEncodings.br = true; + Encodings.supportedEncodings.br = true } -Encodings.reDirective = /^\s*(gzip|compress|deflate|br|identity|\*)\s*(?:;\s*q\s*=\s*(\d(?:\.\d)?))?\s*$/; +Encodings.reDirective = /^\s*(gzip|compress|deflate|br|identity|\*)\s*(?:;\s*q\s*=\s*(\d(?:\.\d*)?))?\s*$/ -Encodings.hasBrotliSupport = hasBrotliSupport; +Encodings.hasBrotliSupport = hasBrotliSupport Encodings.prototype.parseAcceptEncoding = function (acceptEncoding) { - var acceptEncoding = acceptEncoding || ""; + acceptEncoding = acceptEncoding || '' - var encodingWeights = this.encodingWeights, - reDirective = Encodings.reDirective; - acceptEncoding.split(",").forEach(function (directive) { - var match = reDirective.exec(directive); - if (!match) return; // not a supported encoding above + var encodingWeights = this.encodingWeights + var reDirective = Encodings.reDirective + acceptEncoding.split(',').forEach(function (directive) { + var match = reDirective.exec(directive) + if (!match) return // not a supported encoding above - var encoding = match[1]; + var encoding = match[1] // weight must be in [0, 1] - var weight = match[2] && !isNaN(match[2]) ? parseFloat(match[2], 10) : 1; - weight = Math.max(weight, 0); - weight = Math.min(weight, 1); + var weight = match[2] && !isNaN(match[2]) ? parseFloat(match[2], 10) : 1 + weight = Math.max(weight, 0) + weight = Math.min(weight, 1) - encodingWeights.push({ encoding: encoding, weight: weight }); - }); -}; + encodingWeights.push({ encoding: encoding, weight: weight }) + }) +} Encodings.prototype.getPreferredContentEncoding = function () { - var encodingWeights = this.encodingWeights; + var encodingWeights = this.encodingWeights var acceptedEncodings = encodingWeights // sort by weight .sort(function (a, b) { - return b.weight - a.weight; + return b.weight - a.weight }) // filter by supported encodings .filter(function (record) { - return Encodings.supportedEncodings[record.encoding]; - }); + return Encodings.supportedEncodings[record.encoding] + }) // group them by weights - var weightClasses = {}; - var weightList = []; + var weightClasses = {} + var weightList = [] acceptedEncodings.forEach(function (record) { - var weight = record.weight; + var weight = record.weight if (!weightClasses.hasOwnProperty(weight)) { - weightClasses[weight] = []; - weightList.push(weight); + weightClasses[weight] = [] + weightList.push(weight) } - weightClasses[weight].push(record.encoding); - }); + weightClasses[weight].push(record.encoding) + }) // search by weight, descending var weights = weightList.sort(function (a, b) { - return b - a; - }); + return b - a + }) for (var i = 0; i < weights.length; i++) { // encodings at this weight - var encodings = weightClasses[weights[i]]; + var encodings = weightClasses[weights[i]] // return the first encoding in the preferred list for (var j = 0; j < Encodings.preferredEncodings.length; j++) { - var preferredEncoding = Encodings.preferredEncodings[j]; - if (encodings.indexOf(preferredEncoding) >= 0) return preferredEncoding; + var preferredEncoding = Encodings.preferredEncodings[j] + if (encodings.indexOf(preferredEncoding) >= 0) return preferredEncoding } } - // no encoding matches, check to see if the client set identity, q=0 - if (encodingWeights["identity"] && encodingWeights["identity"].weight === 0) { - throw new Error("Please accept br, gzip, deflate, or identity."); - } - // by default, return nothing - return "identity"; -}; + return 'identity' +} diff --git a/test/compression.js b/test/compression.js index 329f13df..028aef05 100644 --- a/test/compression.js +++ b/test/compression.js @@ -592,6 +592,21 @@ describe('compression()', function () { }) }) + describe('when "Accept-Encoding: gzip;q=0.001"', function () { + var brotlit = hasBrotliSupport ? it : it.skip + brotlit('should respond with gzip', function (done) { + var server = createServer({ threshold: 0 }, function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.end('hello, world') + }) + + request(server) + .get('/') + .set('Accept-Encoding', 'gzip;q=0.001') + .expect('Content-Encoding', 'gzip', done) + }) + }) + describe('when "Accept-Encoding: deflate, br"', function () { var brotlit = hasBrotliSupport ? it : it.skip brotlit('should respond with br', function (done) { From 04ab713f0531dd779e5b6cbeb7e3cb2070ef263f Mon Sep 17 00:00:00 2001 From: Nick Randall Date: Mon, 14 Sep 2020 19:37:07 -0400 Subject: [PATCH 14/18] Fix: fixing lint errors in new lib --- lib/encodings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/encodings.js b/lib/encodings.js index 7d43fa5d..4615d842 100644 --- a/lib/encodings.js +++ b/lib/encodings.js @@ -70,7 +70,7 @@ Encodings.prototype.getPreferredContentEncoding = function () { var weightList = [] acceptedEncodings.forEach(function (record) { var weight = record.weight - if (!weightClasses.hasOwnProperty(weight)) { + if (!Object.prototype.hasOwnProperty.call(weightClasses, weight)) { weightClasses[weight] = [] weightList.push(weight) } From b024ccefb82e580ce43447f19bac18d33a4464ce Mon Sep 17 00:00:00 2001 From: Daniel Cohen Gindi Date: Sat, 19 Dec 2020 18:40:33 +0200 Subject: [PATCH 15/18] implemented required encoding negotiator without 3rd party dependency --- encoding_negotiator.js | 78 +++++++++++++++++++++++++++++++++ index.js | 12 +++--- lib/encodings.js | 98 ------------------------------------------ package.json | 1 - 4 files changed, 83 insertions(+), 106 deletions(-) create mode 100644 encoding_negotiator.js delete mode 100644 lib/encodings.js diff --git a/encoding_negotiator.js b/encoding_negotiator.js new file mode 100644 index 00000000..b3b0c7d0 --- /dev/null +++ b/encoding_negotiator.js @@ -0,0 +1,78 @@ +var zlib = require('zlib') + +/** + * @const + * whether current node version has brotli support + */ +var hasBrotliSupport = 'createBrotliCompress' in zlib + +var supportedEncodings = hasBrotliSupport + ? ['br', 'gzip', 'deflate', 'identity'] + : ['gzip', 'deflate', 'identity'] + +var preferredEncodings = hasBrotliSupport + ? ['br', 'gzip'] + : ['gzip'] + +function negotiateEncoding (header) { + header = header || '' + + var insts = header.split(',') + var decoded = [] + + for (var i = 0; i < insts.length; i++) { + var inst = insts[i].match(/^\s*?([^\s;]+?)\s*?(?:;(.*))?$/) + if (!inst) continue + + var encoding = inst[1] + if (supportedEncodings.indexOf(encoding) === -1) { + continue + } + + var q = 1 + if (inst[2]) { + var params = inst[2].split(';') + for (var j = 0; j < params.length; j++) { + var p = params[j].trim().split('=') + if (p[0] === 'q') { + q = parseFloat(p[1]) + break + } + } + } + + if (q < 0 || q > 1) { // invalid + continue + } + + decoded.push({ encoding: encoding, q: q, i: i }) + } + + decoded.sort((a, b) => { + if (a.q !== b.q) { + return b.q - a.q // higher quality first + } + + var aPreferred = preferredEncodings.indexOf(a.encoding) + var bPreferred = preferredEncodings.indexOf(b.encoding) + + if (aPreferred === -1 && bPreferred === -1) { + return a.i - b.i // consider the original order + } + + if (aPreferred !== -1 && bPreferred !== -1) { + return aPreferred - bPreferred // consider the preferred order + } + + return aPreferred === -1 ? 1 : -1 // preferred first + }) + + if (decoded.length > 0) { + return decoded[0].encoding + } + + return null +} + +module.exports.hasBrotliSupport = hasBrotliSupport +module.exports.negotiateEncoding = negotiateEncoding diff --git a/index.js b/index.js index adfc01cd..1eb9ac79 100644 --- a/index.js +++ b/index.js @@ -22,8 +22,8 @@ var objectAssign = require('object-assign') var onHeaders = require('on-headers') var vary = require('vary') var zlib = require('zlib') - -var Encodings = require('./lib/encodings') +var hasBrotliSupport = require('./encoding_negotiator').hasBrotliSupport +var negotiateEncoding = require('./encoding_negotiator').negotiateEncoding /** * Module exports. @@ -50,7 +50,7 @@ var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/ function compression (options) { var opts = options || {} - if (Encodings.hasBrotliSupport) { + if (hasBrotliSupport) { // set the default level to a reasonable value with balanced speed/ratio if (opts.params === undefined) { opts = objectAssign({}, opts) @@ -189,12 +189,10 @@ function compression (options) { } // compression method - var encodings = new Encodings() - encodings.parseAcceptEncoding(req.headers['accept-encoding'] || 'identity') - var method = encodings.getPreferredContentEncoding() + var method = negotiateEncoding(req.headers['accept-encoding']) || 'identity' // negotiation failed - if (!method || method === 'identity') { + if (method === 'identity') { nocompress('not acceptable') return } diff --git a/lib/encodings.js b/lib/encodings.js deleted file mode 100644 index 4615d842..00000000 --- a/lib/encodings.js +++ /dev/null @@ -1,98 +0,0 @@ -// NOTE: Most of this code was ported from "koa-compress" -// See: https://github.com/koajs/compress - -'use strict' -var zlib = require('zlib') - -module.exports = Encodings - -/** - * @const - * whether current node version has brotli support - */ -var hasBrotliSupport = 'createBrotliCompress' in zlib - -function Encodings () { - this.encodingWeights = [] -} - -Encodings.supportedEncodings = { - gzip: true, - deflate: true, - identity: true -} - -Encodings.preferredEncodings = ['br', 'gzip', 'deflate', 'identity'] - -if (hasBrotliSupport) { - Encodings.supportedEncodings.br = true -} - -Encodings.reDirective = /^\s*(gzip|compress|deflate|br|identity|\*)\s*(?:;\s*q\s*=\s*(\d(?:\.\d*)?))?\s*$/ - -Encodings.hasBrotliSupport = hasBrotliSupport - -Encodings.prototype.parseAcceptEncoding = function (acceptEncoding) { - acceptEncoding = acceptEncoding || '' - - var encodingWeights = this.encodingWeights - var reDirective = Encodings.reDirective - acceptEncoding.split(',').forEach(function (directive) { - var match = reDirective.exec(directive) - if (!match) return // not a supported encoding above - - var encoding = match[1] - - // weight must be in [0, 1] - var weight = match[2] && !isNaN(match[2]) ? parseFloat(match[2], 10) : 1 - weight = Math.max(weight, 0) - weight = Math.min(weight, 1) - - encodingWeights.push({ encoding: encoding, weight: weight }) - }) -} - -Encodings.prototype.getPreferredContentEncoding = function () { - var encodingWeights = this.encodingWeights - - var acceptedEncodings = encodingWeights - // sort by weight - .sort(function (a, b) { - return b.weight - a.weight - }) - // filter by supported encodings - .filter(function (record) { - return Encodings.supportedEncodings[record.encoding] - }) - - // group them by weights - var weightClasses = {} - var weightList = [] - acceptedEncodings.forEach(function (record) { - var weight = record.weight - if (!Object.prototype.hasOwnProperty.call(weightClasses, weight)) { - weightClasses[weight] = [] - weightList.push(weight) - } - weightClasses[weight].push(record.encoding) - }) - - // search by weight, descending - var weights = weightList.sort(function (a, b) { - return b - a - }) - - for (var i = 0; i < weights.length; i++) { - // encodings at this weight - var encodings = weightClasses[weights[i]] - - // return the first encoding in the preferred list - for (var j = 0; j < Encodings.preferredEncodings.length; j++) { - var preferredEncoding = Encodings.preferredEncodings[j] - if (encodings.indexOf(preferredEncoding) >= 0) return preferredEncoding - } - } - - // by default, return nothing - return 'identity' -} diff --git a/package.json b/package.json index 58196d81..a202e9a7 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "license": "MIT", "repository": "expressjs/compression", "dependencies": { - "accepts": "~1.3.7", "bytes": "3.0.0", "compressible": "~2.0.17", "debug": "2.6.9", From 25b68b8f62fa823e712d850b46781a9ab9d769ab Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 19 Oct 2024 07:03:35 -0500 Subject: [PATCH 16/18] fix --- encoding_negotiator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/encoding_negotiator.js b/encoding_negotiator.js index b3b0c7d0..e66567ef 100644 --- a/encoding_negotiator.js +++ b/encoding_negotiator.js @@ -48,7 +48,7 @@ function negotiateEncoding (header) { decoded.push({ encoding: encoding, q: q, i: i }) } - decoded.sort((a, b) => { + decoded.sort(function (a, b) { if (a.q !== b.q) { return b.q - a.q // higher quality first } From 3d30ab0b34ca91695c328dfde14b795510131e2c Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 19 Oct 2024 15:09:54 -0500 Subject: [PATCH 17/18] use negotiator --- encoding_negotiator.js | 73 ++++++++---------------------------------- index.js | 9 ++++-- package.json | 2 +- test/compression.js | 9 +----- 4 files changed, 22 insertions(+), 71 deletions(-) diff --git a/encoding_negotiator.js b/encoding_negotiator.js index e66567ef..06b0422b 100644 --- a/encoding_negotiator.js +++ b/encoding_negotiator.js @@ -1,4 +1,5 @@ var zlib = require('zlib') +var Negotiator = require('negotiator') /** * @const @@ -6,72 +7,24 @@ var zlib = require('zlib') */ var hasBrotliSupport = 'createBrotliCompress' in zlib -var supportedEncodings = hasBrotliSupport - ? ['br', 'gzip', 'deflate', 'identity'] - : ['gzip', 'deflate', 'identity'] +function negotiateEncoding (req, encodings_) { + var negotiator = new Negotiator(req) + var encodings = encodings_ -var preferredEncodings = hasBrotliSupport - ? ['br', 'gzip'] - : ['gzip'] - -function negotiateEncoding (header) { - header = header || '' - - var insts = header.split(',') - var decoded = [] - - for (var i = 0; i < insts.length; i++) { - var inst = insts[i].match(/^\s*?([^\s;]+?)\s*?(?:;(.*))?$/) - if (!inst) continue - - var encoding = inst[1] - if (supportedEncodings.indexOf(encoding) === -1) { - continue + // support flattened arguments + if (encodings && !Array.isArray(encodings)) { + encodings = new Array(arguments.length) + for (var i = 0; i < encodings.length; i++) { + encodings[i] = arguments[i] } - - var q = 1 - if (inst[2]) { - var params = inst[2].split(';') - for (var j = 0; j < params.length; j++) { - var p = params[j].trim().split('=') - if (p[0] === 'q') { - q = parseFloat(p[1]) - break - } - } - } - - if (q < 0 || q > 1) { // invalid - continue - } - - decoded.push({ encoding: encoding, q: q, i: i }) } - decoded.sort(function (a, b) { - if (a.q !== b.q) { - return b.q - a.q // higher quality first - } - - var aPreferred = preferredEncodings.indexOf(a.encoding) - var bPreferred = preferredEncodings.indexOf(b.encoding) - - if (aPreferred === -1 && bPreferred === -1) { - return a.i - b.i // consider the original order - } - - if (aPreferred !== -1 && bPreferred !== -1) { - return aPreferred - bPreferred // consider the preferred order - } - - return aPreferred === -1 ? 1 : -1 // preferred first - }) - - if (decoded.length > 0) { - return decoded[0].encoding + // no encodings, return all requested encodings + if (!encodings || encodings.length === 0) { + return negotiator.encodings() } - return null + return negotiator.encodings(encodings, hasBrotliSupport ? ['br'] : ['gzip'])[0] || false } module.exports.hasBrotliSupport = hasBrotliSupport diff --git a/index.js b/index.js index 1eb9ac79..77fb7f80 100644 --- a/index.js +++ b/index.js @@ -189,10 +189,15 @@ function compression (options) { } // compression method - var method = negotiateEncoding(req.headers['accept-encoding']) || 'identity' + var method = negotiateEncoding(req, ['br', 'gzip', 'deflate', 'identity']) + + // we really don't prefer deflate + if (method === 'deflate' && negotiateEncoding(req, ['gzip'])) { + method = negotiateEncoding(req, ['br', 'gzip', 'identity']) + } // negotiation failed - if (method === 'identity') { + if (!method || method === 'identity') { nocompress('not acceptable') return } diff --git a/package.json b/package.json index 7e225094..91802aaa 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "license": "MIT", "repository": "expressjs/compression", "dependencies": { - "accepts": "~1.3.8", + "negotiator": "0.6.4", "bytes": "3.0.0", "compressible": "~2.0.18", "debug": "2.6.9", diff --git a/test/compression.js b/test/compression.js index 028aef05..0ceaa63e 100644 --- a/test/compression.js +++ b/test/compression.js @@ -14,6 +14,7 @@ var compression = require('..') * whether current node version has brotli support */ var hasBrotliSupport = 'createBrotliCompress' in zlib +var brotlit = hasBrotliSupport ? it : it.skip describe('compression()', function () { it('should skip HEAD', function (done) { @@ -472,7 +473,6 @@ describe('compression()', function () { }) describe('when "Accept-Encoding: br"', function () { - var brotlit = hasBrotliSupport ? it : it.skip brotlit('should respond with br', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') @@ -487,7 +487,6 @@ describe('compression()', function () { }) describe('when "Accept-Encoding: br" and passing compression level', function () { - var brotlit = hasBrotliSupport ? it : it.skip brotlit('should respond with br', function (done) { var params = {} params[zlib.constants.BROTLI_PARAM_QUALITY] = 11 @@ -548,7 +547,6 @@ describe('compression()', function () { }) describe('when "Accept-Encoding: deflate, gzip, br"', function () { - var brotlit = hasBrotliSupport ? it : it.skip brotlit('should respond with br', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') @@ -563,7 +561,6 @@ describe('compression()', function () { }) describe('when "Accept-Encoding: gzip;q=1, br;q=0.3"', function () { - var brotlit = hasBrotliSupport ? it : it.skip brotlit('should respond with gzip', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') @@ -578,7 +575,6 @@ describe('compression()', function () { }) describe('when "Accept-Encoding: gzip, br;q=0.8"', function () { - var brotlit = hasBrotliSupport ? it : it.skip brotlit('should respond with gzip', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') @@ -593,7 +589,6 @@ describe('compression()', function () { }) describe('when "Accept-Encoding: gzip;q=0.001"', function () { - var brotlit = hasBrotliSupport ? it : it.skip brotlit('should respond with gzip', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') @@ -608,7 +603,6 @@ describe('compression()', function () { }) describe('when "Accept-Encoding: deflate, br"', function () { - var brotlit = hasBrotliSupport ? it : it.skip brotlit('should respond with br', function (done) { var server = createServer({ threshold: 0 }, function (req, res) { res.setHeader('Content-Type', 'text/plain') @@ -760,7 +754,6 @@ describe('compression()', function () { .end() }) - var brotlit = hasBrotliSupport ? it : it.skip brotlit('should flush small chunks for brotli', function (done) { var chunks = 0 var next From 662c09d49ffbcf8c28b0860280f0c2615bf7c148 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 23 Oct 2024 19:14:19 -0500 Subject: [PATCH 18/18] improve negotiateEnconding --- encoding_negotiator.js | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/encoding_negotiator.js b/encoding_negotiator.js index 06b0422b..3ef949fc 100644 --- a/encoding_negotiator.js +++ b/encoding_negotiator.js @@ -7,24 +7,10 @@ var Negotiator = require('negotiator') */ var hasBrotliSupport = 'createBrotliCompress' in zlib -function negotiateEncoding (req, encodings_) { +function negotiateEncoding (req, encodings) { var negotiator = new Negotiator(req) - var encodings = encodings_ - // support flattened arguments - if (encodings && !Array.isArray(encodings)) { - encodings = new Array(arguments.length) - for (var i = 0; i < encodings.length; i++) { - encodings[i] = arguments[i] - } - } - - // no encodings, return all requested encodings - if (!encodings || encodings.length === 0) { - return negotiator.encodings() - } - - return negotiator.encodings(encodings, hasBrotliSupport ? ['br'] : ['gzip'])[0] || false + return negotiator.encodings(encodings, hasBrotliSupport ? ['br'] : ['gzip'])[0] } module.exports.hasBrotliSupport = hasBrotliSupport