Skip to content

Commit

Permalink
feat: Adding support for brolti
Browse files Browse the repository at this point in the history
  • Loading branch information
nicksrandall committed Sep 15, 2020
1 parent 3fea81d commit 42ea97b
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 9 deletions.
20 changes: 19 additions & 1 deletion README.md
Expand Up @@ -11,6 +11,9 @@ The following compression codings are supported:

- deflate
- gzip
- br (brotli)

**Note** Brotli is supported only since Node.js versions v11.7.0 and v10.16.0.

## Install

Expand Down Expand Up @@ -44,7 +47,8 @@ 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.

##### chunkSize

Expand Down Expand Up @@ -101,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)* - [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)
- `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
Expand Down
78 changes: 70 additions & 8 deletions index.js
Expand Up @@ -22,6 +22,7 @@ var debug = require('debug')('compression')
var onHeaders = require('on-headers')
var vary = require('vary')
var zlib = require('zlib')
var objectAssign = require('object-assign')

/**
* Module exports.
Expand All @@ -30,6 +31,12 @@ var zlib = require('zlib')
module.exports = compression
module.exports.filter = shouldCompress

/**
* @const
* whether current node version has brotli support
*/
var hasBrotliSupport = 'createBrotliCompress' in zlib

/**
* Module variables.
* @private
Expand All @@ -48,6 +55,17 @@ var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
function compression (options) {
var opts = options || {}

if (hasBrotliSupport) {
// set the default level to a reasonable value with balanced speed/ratio
if (opts.params === undefined) {
opts.params = {}
}

if (opts.params[zlib.constants.BROTLI_PARAM_QUALITY] === undefined) {
opts.params[zlib.constants.BROTLI_PARAM_QUALITY] = 4
}
}

// options
var filter = opts.filter || shouldCompress
var threshold = bytes.parse(opts.threshold)
Expand Down Expand Up @@ -173,14 +191,12 @@ function compression (options) {
return
}

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

// we really don't prefer deflate
if (method === 'deflate' && accept.encoding(['gzip'])) {
method = accept.encoding(['gzip', 'identity'])
}
// compression method
var accept = accepts(objectAssign({}, res, { headers: headers }))
var method = accept.encoding(['br', 'gzip', 'deflate', 'identity'])

// negotiation failed
if (!method || method === 'identity') {
Expand All @@ -192,7 +208,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)
Expand Down Expand Up @@ -286,3 +304,47 @@ function toBuffer (chunk, encoding) {
? Buffer.from(chunk, encoding)
: chunk
}

/**
* Most browsers send "br" (brolti) as the last value in
* in the 'Accept-Encoding' header which causes it to be
* depriorited according to the spec.
*
* This is typically not what end users actually want so here
* we force the "br" (brotli) value to first in the list so that
* it will get properly prioritized and used.
*
* It's worth noting that although this is not "spec compliant",
* we belive it follows a well-established convention.
*
* @private
*/
function prioritize (str) {
return str && str.split(',')
.sort(sortAlgs)
.join(',')
}

/**
* Sort compression algs in order of preference:
* br > gzip > deflate | identity
*
* @private
*/
function sortAlgs (a, b) {
if (a.indexOf('br') >= 0) {
return -1
}
if (a.indexOf('gzip') >= 0) {
return b.indexOf('br') >= 0 ? 1 : -1
}
// we need these inverse rules to fix a stable sort bug
// found in node 10.x
if (b.indexOf('br') >= 0) {
return 1
}
if (b.indexOf('gzip') >= 0) {
return a.indexOf('br') >= 0 ? -1 : 1
}
return 0
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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"
Expand Down
155 changes: 155 additions & 0 deletions test/compression.js
Expand Up @@ -9,6 +9,8 @@ var zlib = require('zlib')

var compression = require('..')

var hasBrotliSupport = 'createBrotliCompress' in zlib

describe('compression()', function () {
it('should skip HEAD', function (done) {
var server = createServer({ threshold: 0 }, function (req, res) {
Expand Down Expand Up @@ -656,6 +658,156 @@ 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()
})
})

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: 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, 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: 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: 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: 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) {
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)
})
})
})

Expand Down Expand Up @@ -710,6 +862,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)
Expand Down

0 comments on commit 42ea97b

Please sign in to comment.