Skip to content

Commit ceb509a

Browse files
committed
feat: Adding support for brolti
1 parent 3fea81d commit ceb509a

File tree

4 files changed

+290
-9
lines changed

4 files changed

+290
-9
lines changed

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ The following compression codings are supported:
1111

1212
- deflate
1313
- gzip
14+
- br (brotli)
15+
16+
**Note** Brotli is supported only since Node.js versions v11.7.0 and v10.16.0.
1417

1518
## Install
1619

@@ -44,7 +47,8 @@ as compressing will transform the body.
4447

4548
`compression()` accepts these properties in the options object. In addition to
4649
those listed below, [zlib](http://nodejs.org/api/zlib.html) options may be
47-
passed in to the options object.
50+
passed in to the options object or
51+
[brotli](https://nodejs.org/api/zlib.html#zlib_class_brotlioptions) options.
4852

4953
##### chunkSize
5054

@@ -101,6 +105,20 @@ The default value is `zlib.Z_DEFAULT_MEMLEVEL`, or `8`.
101105
See [Node.js documentation](http://nodejs.org/api/zlib.html#zlib_memory_usage_tuning)
102106
regarding the usage.
103107

108+
##### params *(brotli only)* - [key-value object containing indexed Brotli parameters](https://nodejs.org/api/zlib.html#zlib_brotli_constants)
109+
110+
- `zlib.constants.BROTLI_PARAM_MODE`
111+
- `zlib.constants.BROTLI_MODE_GENERIC` (default)
112+
- `zlib.constants.BROTLI_MODE_TEXT`, adjusted for UTF-8 text
113+
- `zlib.constants.BROTLI_MODE_FONT`, adjusted for WOFF 2.0 fonts
114+
- `zlib.constants.BROTLI_PARAM_QUALITY`
115+
- Ranges from `zlib.constants.BROTLI_MIN_QUALITY` to
116+
`zlib.constants.BROTLI_MAX_QUALITY`, with a default of
117+
`4` (which is not node's default but the most optimal).
118+
119+
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
120+
compression ratio.
121+
104122
##### strategy
105123

106124
This is used to tune the compression algorithm. This value only affects the

index.js

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var debug = require('debug')('compression')
2222
var onHeaders = require('on-headers')
2323
var vary = require('vary')
2424
var zlib = require('zlib')
25+
var objectAssign = require('object-assign')
2526

2627
/**
2728
* Module exports.
@@ -30,6 +31,12 @@ var zlib = require('zlib')
3031
module.exports = compression
3132
module.exports.filter = shouldCompress
3233

34+
/**
35+
* @const
36+
* whether current node version has brotli support
37+
*/
38+
var hasBrotliSupport = 'createBrotliCompress' in zlib
39+
3340
/**
3441
* Module variables.
3542
* @private
@@ -48,6 +55,17 @@ var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
4855
function compression (options) {
4956
var opts = options || {}
5057

58+
if (hasBrotliSupport) {
59+
// set the default level to a reasonable value with balanced speed/ratio
60+
if (opts.params === undefined) {
61+
opts.params = {}
62+
}
63+
64+
if (opts.params[zlib.constants.BROTLI_PARAM_QUALITY] === undefined) {
65+
opts.params[zlib.constants.BROTLI_PARAM_QUALITY] = 4
66+
}
67+
}
68+
5169
// options
5270
var filter = opts.filter || shouldCompress
5371
var threshold = bytes.parse(opts.threshold)
@@ -173,14 +191,12 @@ function compression (options) {
173191
return
174192
}
175193

176-
// compression method
177-
var accept = accepts(req)
178-
var method = accept.encoding(['gzip', 'deflate', 'identity'])
194+
// force proper priorization
195+
var headers = objectAssign({}, req.headers, { 'accept-encoding': prioritize(req.headers['accept-encoding']) })
179196

180-
// we really don't prefer deflate
181-
if (method === 'deflate' && accept.encoding(['gzip'])) {
182-
method = accept.encoding(['gzip', 'identity'])
183-
}
197+
// compression method
198+
var accept = accepts(objectAssign({}, res, { headers: headers }))
199+
var method = accept.encoding(['br', 'gzip', 'deflate', 'identity'])
184200

185201
// negotiation failed
186202
if (!method || method === 'identity') {
@@ -192,7 +208,9 @@ function compression (options) {
192208
debug('%s compression', method)
193209
stream = method === 'gzip'
194210
? zlib.createGzip(opts)
195-
: zlib.createDeflate(opts)
211+
: method === 'br'
212+
? zlib.createBrotliCompress(opts)
213+
: zlib.createDeflate(opts)
196214

197215
// add buffered listeners to stream
198216
addListeners(stream, stream.on, listeners)
@@ -286,3 +304,47 @@ function toBuffer (chunk, encoding) {
286304
? Buffer.from(chunk, encoding)
287305
: chunk
288306
}
307+
308+
/**
309+
* Most browsers send "br" (brolti) as the last value in
310+
* in the 'Accept-Encoding' header which causes it to be
311+
* depriorited according to the spec.
312+
*
313+
* This is typically not what end users actually want so here
314+
* we force the "br" (brotli) value to first in the list so that
315+
* it will get properly prioritized and used.
316+
*
317+
* It's worth noting that although this is not "spec compliant",
318+
* we belive it follows a well-established convention.
319+
*
320+
* @private
321+
*/
322+
function prioritize (str) {
323+
return str && str.split(',')
324+
.sort(sortAlgs)
325+
.join(',')
326+
}
327+
328+
/**
329+
* Sort compression algs in order of preference:
330+
* br > gzip > deflate | identity
331+
*
332+
* @private
333+
*/
334+
function sortAlgs (a, b) {
335+
if (a.indexOf('br') >= 0) {
336+
return -1
337+
}
338+
if (a.indexOf('gzip') >= 0) {
339+
return b.indexOf('br') >= 0 ? 1 : -1
340+
}
341+
// we need these inverse rules to fix a stable sort bug
342+
// found in node 10.x
343+
if (b.indexOf('br') >= 0) {
344+
return 1
345+
}
346+
if (b.indexOf('gzip') >= 0) {
347+
return a.indexOf('br') >= 0 ? -1 : 1
348+
}
349+
return 0
350+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"bytes": "3.0.0",
1414
"compressible": "~2.0.17",
1515
"debug": "2.6.9",
16+
"object-assign": "4.1.1",
1617
"on-headers": "~1.0.2",
1718
"safe-buffer": "5.2.0",
1819
"vary": "~1.1.2"

test/compression.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ var zlib = require('zlib')
99

1010
var compression = require('..')
1111

12+
var hasBrotliSupport = 'createBrotliCompress' in zlib
13+
1214
describe('compression()', function () {
1315
it('should skip HEAD', function (done) {
1416
var server = createServer({ threshold: 0 }, function (req, res) {
@@ -656,6 +658,201 @@ describe('compression()', function () {
656658
}))
657659
.end()
658660
})
661+
662+
var brotlit = hasBrotliSupport ? it : it.skip
663+
brotlit('should flush small chunks for brotli', function (done) {
664+
var chunks = 0
665+
var next
666+
var server = createServer({ threshold: 0 }, function (req, res) {
667+
next = writeAndFlush(res, 2, Buffer.from('..'))
668+
res.setHeader('Content-Type', 'text/plain')
669+
next()
670+
})
671+
672+
function onchunk (chunk) {
673+
assert.ok(chunks++ < 20)
674+
assert.strictEqual(chunk.toString(), '..')
675+
next()
676+
}
677+
678+
request(server)
679+
.get('/')
680+
.set('Accept-Encoding', 'br')
681+
.request()
682+
.on('response', unchunk('br', onchunk, function (err) {
683+
if (err) return done(err)
684+
server.close(done)
685+
}))
686+
.end()
687+
})
688+
})
689+
690+
describe('when "Accept-Encoding: br"', function () {
691+
var brotlit = hasBrotliSupport ? it : it.skip
692+
brotlit('should respond with br', function (done) {
693+
var server = createServer({ threshold: 0 }, function (req, res) {
694+
res.setHeader('Content-Type', 'text/plain')
695+
res.end('hello, world')
696+
})
697+
698+
request(server)
699+
.get('/')
700+
.set('Accept-Encoding', 'br')
701+
.expect('Content-Encoding', 'br', done)
702+
})
703+
})
704+
705+
describe('when "Accept-Encoding: br" and passing compression level', function () {
706+
var brotlit = hasBrotliSupport ? it : it.skip
707+
brotlit('should respond with br', function (done) {
708+
var params = {}
709+
params[zlib.constants.BROTLI_PARAM_QUALITY] = 11
710+
711+
var server = createServer({ threshold: 0, params: params }, function (req, res) {
712+
res.setHeader('Content-Type', 'text/plain')
713+
res.end('hello, world')
714+
})
715+
716+
request(server)
717+
.get('/')
718+
.set('Accept-Encoding', 'br')
719+
.expect('Content-Encoding', 'br', done)
720+
})
721+
})
722+
723+
describe('when "Accept-Encoding: gzip, br"', function () {
724+
var brotlit = hasBrotliSupport ? it : it.skip
725+
brotlit('should respond with br', function (done) {
726+
var server = createServer({ threshold: 0 }, function (req, res) {
727+
res.setHeader('Content-Type', 'text/plain')
728+
res.end('hello, world')
729+
})
730+
731+
request(server)
732+
.get('/')
733+
.set('Accept-Encoding', 'gzip, br')
734+
.expect('Content-Encoding', 'br', done)
735+
})
736+
})
737+
738+
describe('when "Accept-Encoding: deflate, gzip, br"', function () {
739+
var brotlit = hasBrotliSupport ? it : it.skip
740+
brotlit('should respond with br', function (done) {
741+
var server = createServer({ threshold: 0 }, function (req, res) {
742+
res.setHeader('Content-Type', 'text/plain')
743+
res.end('hello, world')
744+
})
745+
746+
request(server)
747+
.get('/')
748+
.set('Accept-Encoding', 'deflate, gzip, br')
749+
.expect('Content-Encoding', 'br', done)
750+
})
751+
})
752+
753+
describe('when "Accept-Encoding: gzip;q=1, br;q=0.3"', function () {
754+
var brotlit = hasBrotliSupport ? it : it.skip
755+
brotlit('should respond with gzip', function (done) {
756+
var server = createServer({ threshold: 0 }, function (req, res) {
757+
res.setHeader('Content-Type', 'text/plain')
758+
res.end('hello, world')
759+
})
760+
761+
request(server)
762+
.get('/')
763+
.set('Accept-Encoding', 'gzip;q=1, br;q=0.3')
764+
.expect('Content-Encoding', 'gzip', done)
765+
})
766+
})
767+
768+
describe('when "Accept-Encoding: gzip, br;q=0.8"', function () {
769+
var brotlit = hasBrotliSupport ? it : it.skip
770+
brotlit('should respond with gzip', function (done) {
771+
var server = createServer({ threshold: 0 }, function (req, res) {
772+
res.setHeader('Content-Type', 'text/plain')
773+
res.end('hello, world')
774+
})
775+
776+
request(server)
777+
.get('/')
778+
.set('Accept-Encoding', 'gzip, br;q=0.8')
779+
.expect('Content-Encoding', 'gzip', done)
780+
})
781+
})
782+
783+
describe('when "Accept-Encoding: gzip;q=0.001"', function () {
784+
var brotlit = hasBrotliSupport ? it : it.skip
785+
brotlit('should respond with gzip', function (done) {
786+
var server = createServer({ threshold: 0 }, function (req, res) {
787+
res.setHeader('Content-Type', 'text/plain')
788+
res.end('hello, world')
789+
})
790+
791+
request(server)
792+
.get('/')
793+
.set('Accept-Encoding', 'gzip;q=0.001')
794+
.expect('Content-Encoding', 'gzip', done)
795+
})
796+
})
797+
798+
describe('when "Accept-Encoding: deflate, br"', function () {
799+
var brotlit = hasBrotliSupport ? it : it.skip
800+
brotlit('should respond with br', function (done) {
801+
var server = createServer({ threshold: 0 }, function (req, res) {
802+
res.setHeader('Content-Type', 'text/plain')
803+
res.end('hello, world')
804+
})
805+
806+
request(server)
807+
.get('/')
808+
.set('Accept-Encoding', 'deflate, br')
809+
.expect('Content-Encoding', 'br', done)
810+
})
811+
})
812+
813+
describe('when "Accept-Encoding: deflate, br, gzip"', function () {
814+
var brotlit = hasBrotliSupport ? it : it.skip
815+
brotlit('should respond with br', function (done) {
816+
var server = createServer({ threshold: 0 }, function (req, res) {
817+
res.setHeader('Content-Type', 'text/plain')
818+
res.end('hello, world')
819+
})
820+
821+
request(server)
822+
.get('/')
823+
.set('Accept-Encoding', 'deflate, br, gzip')
824+
.expect('Content-Encoding', 'br', done)
825+
})
826+
})
827+
828+
describe('when "Accept-Encoding: deflate, gzip"', function () {
829+
var brotlit = hasBrotliSupport ? it : it.skip
830+
brotlit('should respond with br', function (done) {
831+
var server = createServer({ threshold: 0 }, function (req, res) {
832+
res.setHeader('Content-Type', 'text/plain')
833+
res.end('hello, world')
834+
})
835+
836+
request(server)
837+
.get('/')
838+
.set('Accept-Encoding', 'deflate, gzip')
839+
.expect('Content-Encoding', 'gzip', done)
840+
})
841+
})
842+
843+
describe('when "Accept-Encoding: deflate, identity"', function () {
844+
var brotlit = hasBrotliSupport ? it : it.skip
845+
brotlit('should respond with br', function (done) {
846+
var server = createServer({ threshold: 0 }, function (req, res) {
847+
res.setHeader('Content-Type', 'text/plain')
848+
res.end('hello, world')
849+
})
850+
851+
request(server)
852+
.get('/')
853+
.set('Accept-Encoding', 'deflate, identity')
854+
.expect('Content-Encoding', 'deflate', done)
855+
})
659856
})
660857
})
661858

@@ -710,6 +907,9 @@ function unchunk (encoding, onchunk, onend) {
710907
case 'gzip':
711908
stream = res.pipe(zlib.createGunzip())
712909
break
910+
case 'br':
911+
stream = res.pipe(zlib.createBrotliDecompress())
912+
break
713913
}
714914

715915
stream.on('data', onchunk)

0 commit comments

Comments
 (0)